Compare commits

...

47 Commits

Author SHA1 Message Date
Nguyễn Gia Phong e860f1526d Remove Travis CI
Ubuntu Trusty doesn't provide packages needed for testing, namely libmpv1
and Python 3.7+.
2018-09-19 17:19:47 +07:00
Nguyễn Gia Phong 6576c01801 Clean up and update Travis intergration 2018-09-18 20:57:14 +07:00
cclauss 900a3f359a Use Travis CI to run Continuous Integration tests 2018-04-22 09:53:56 +07:00
Nguyễn Gia Phong 7203eaaef5 Remove undefined names in omp/omp.py 2018-04-22 09:53:56 +07:00
cclauss 51ba038b80 import curses, re in omp/omp.py 2018-04-22 09:53:21 +07:00
Nguyễn Gia Phong be0eaeadb7 Give more sense to shuffle-* and *-selected modes (#8) 2018-01-30 17:55:27 +07:00
Nguyễn Gia Phong f8185d1b31 Make comp ask to dump when quit and dump less bullsh*t 2018-01-30 17:08:47 +07:00
Nguyễn Gia Phong 082685cee3 Fix KeyError when there isn't mpv section in config file (#7) 2018-01-30 09:40:22 +07:00
Nguyễn Gia Phong 0500d98b0a Add support for mpv arguments in config file (#6) 2018-01-28 23:12:13 +07:00
Nguyễn Gia Phong bb29631789 Update keybindings documentation 2018-01-25 22:09:23 +07:00
Nguyễn Gia Phong f4791e6e99 Add warning messages 2018-01-25 22:09:23 +07:00
Nguyễn Gia Phong 37f92bcc74 Fix duplicated keybindings 2018-01-25 22:09:23 +07:00
Nguyễn Gia Phong e21fd6285d Make compatible with python-mpv 0.3.5 and edit search keybindings 2018-01-25 22:09:23 +07:00
Nguyễn Gia Phong 4b18b5c1bf Start implementing default mpv keybindings 2018-01-25 22:09:23 +07:00
Nguyễn Gia Phong 6ada63f856 Clean up 2018-01-25 22:09:22 +07:00
Nguyễn Gia Phong b01b6abd1a Make compatible with python-mpv 0.3 2018-01-25 22:09:22 +07:00
Nguyễn Gia Phong 7542fb3892 Change mpv and youtube-dl links 2018-01-25 22:09:22 +07:00
Nguyễn Gia Phong 1b0692417f Update documentation and drop gettext support for argparse help strings 2018-01-25 22:09:22 +07:00
Nguyễn Gia Phong ba3a065006 Fix JSON playlist dumper and edit keybindings 2018-01-25 22:09:22 +07:00
Nguyễn Gia Phong 04901f33d9 Update documentation 2018-01-25 22:09:22 +07:00
Nguyễn Gia Phong 9cef1e2382 Fix class calling 2018-01-25 22:09:22 +07:00
Nguyễn Gia Phong 2972111b59 Fix youtube-dl stderr garbage printing and edit installation installation instruction 2018-01-25 22:09:22 +07:00
Nguyễn Gia Phong 6d0aa7fe51 Update documentations and clean up (?) 2018-01-25 22:09:22 +07:00
Nguyễn Gia Phong 801c439146 Update Vietnamese translation 2018-01-25 22:09:22 +07:00
Nguyễn Gia Phong 031f9ea1aa Improve info extractor 2018-01-25 22:09:22 +07:00
Nguyễn Gia Phong 654c5572ef Improve printing (slightly better performance) 2018-01-25 22:09:22 +07:00
Nguyễn Gia Phong 9129a8974f Clean up everything 2018-01-25 22:09:21 +07:00
Nguyễn Gia Phong 98c73ae8ac Code clean-up 2018-01-25 22:09:21 +07:00
Nguyễn Gia Phong 0bca71fe0c Begin to create an independant back-end object 2018-01-25 22:09:21 +07:00
Nguyễn Gia Phong 8334263141 Sync with jaseg/python-mpv 2018-01-25 22:09:21 +07:00
Nguyễn Gia Phong 056727768d Add search function 2018-01-25 22:09:21 +07:00
Nguyễn Gia Phong d146f5d74c Refine codebase 2018-01-25 22:09:21 +07:00
Nguyễn Gia Phong df98e90366 Refactor partially (now more things are broken) 2018-01-25 22:09:21 +07:00
Nguyễn Gia Phong cf9cfd3c01 Create an object to avoid using global variable 2018-01-25 22:09:21 +07:00
Nguyễn Gia Phong fc6b4a0c51 Fix the bug when the playlist is shorter than the screen 2018-01-25 22:09:21 +07:00
Nguyễn Gia Phong c1b0652078 Import youtube-dl 2018-01-25 22:09:21 +07:00
Nguyễn Gia Phong 6a2c20e490 Add save playlist function and complete Vietnamese translation 2018-01-25 22:09:21 +07:00
Nguyễn Gia Phong 3679965578 Add Youtube playlist argument, seek function and patial support for translation using gettext 2018-01-25 22:09:21 +07:00
Nguyễn Gia Phong 23fc9ae49b Update documentation and setup 2018-01-25 22:09:21 +07:00
Nguyễn Gia Phong 5abe0b2ebc Add repeat and shuffle mode 2018-01-25 22:09:21 +07:00
Nguyễn Gia Phong 3f603ed693 Add playing title to statusline and audio, video toggle feature 2018-01-25 22:09:20 +07:00
Nguyễn Gia Phong d5117c65c8 Add pause function and time-pos 2018-01-25 22:09:20 +07:00
Nguyễn Gia Phong 1b130724d2 Standardize function calls 2018-01-25 22:09:20 +07:00
Nguyễn Gia Phong c9fcb30f75 Refactor to not use curses.wrapper 2018-01-25 22:09:20 +07:00
Nguyễn Gia Phong 1e1418f8dd Add bottom panel and improve data structure 2018-01-25 22:09:20 +07:00
Nguyễn Gia Phong 7066eeb697 Initial comp commit 2018-01-25 22:09:20 +07:00
jaseg f8b6ac8f66 Rebase python-mpv commits
Now with mpv.py moved out of the repository, comp isn't any longer a
fork of python-mpv. These commits confuses the log and thus got squashed
into one.
2018-01-25 22:06:26 +07:00
19 changed files with 1875 additions and 609 deletions

4
.gitignore vendored
View File

@ -1,3 +1,5 @@
MANIFEST
build
comp.egg-info
dist
mpv.egg-info
__pycache__

1
MANIFEST.in Normal file
View File

@ -0,0 +1 @@
include LICENSE README.rst doc/screenshot.png

View File

@ -1,4 +0,0 @@
python-mpv
==========
python-mpv is a ctypes-based python interface to the mpv media player. It gives you more or less full control of all features of the player, just as the lua interface does.

297
README.rst Normal file
View File

@ -0,0 +1,297 @@
===============================
comp - Curses Omni Media Player
===============================
**comp** is a `mpv <https://mpv.io/>`_ 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 <https://rg3.github.io/youtube-dl/>`_,
local and direct URL to video/audio and its own JSON playlist format.
.. image:: https://github.com/McSinyx/comp/raw/master/doc/screenshot.png
Installation
------------
**comp** requires Python 3.5+ with ``curses`` module (only available on
Unix-like OSes such as GNU/Linux and the BSDs) and ``libmpv`` (available as
``libmpv1`` in Debian/Ubuntu, openSUSE; and as ``mpv`` in Arch Linux, Gentoo,
macOS Homebrew repository). It also depends on ``python-mpv`` and
``youtube-dl`` but the setup program will automatically install them if they
are missing.
As ``setuptools`` will `install in an egg and cause breakage
<https://github.com/McSinyx/comp/issues/5>`_, using ``pip`` (Python 3 version)
is a must. After `installing it <https://pip.pypa.io/en/latest/installing/>`_,
run ``pip3 install comp`` (you might want to add ``--user`` flag to use the
`User Scheme <https://pip.pypa.io/en/stable/user_guide/#user-installs>`_).
For developers, clone the `Github repo <https://github.com/McSinyx/comp>`_ then
simply run the ``comp`` executable to test the program. If you insist on
installing it, still use ``pip3``: ``pip3 install .``. Note that **comp** is
distibuted in a ``wheel`` created via ``./setup.py bdist_wheel``.
Command line options
--------------------
::
usage: comp [-h] [-v] [-e {json,mpv,youtube-dl}] [-c CONFIG] [--vid VID]
[--vo DRIVER] [-f YTDL_FORMAT]
playlist
Curses Omni Media Player
positional arguments:
playlist path or URL to the playlist
optional arguments:
-h, --help show this help message and exit
-v, --version show program's version number and exit
-e {json,mpv,youtube-dl}, --extractor {json,mpv,youtube-dl}
playlist extractor, default is youtube-dl
-c CONFIG, --config CONFIG
path to the configuration file
--vid VID initial video channel. auto selects the default, no
disables video
--vo DRIVER specify the video output backend to be used. See
VIDEO OUTPUT DRIVERS in mpv(1) for details and
descriptions of available drivers
-f YTDL_FORMAT, --format YTDL_FORMAT
video format/quality to be passed to youtube-dl
Examples
^^^^^^^^
Open a JSON playlist::
comp -e json test/playlist.json
Open a Youtube playlist with video height lower than 720::
comp -f '[height<720]' https://www.youtube.com/watch?list=PLnk14Iku8QM7R3ARnrj1TwYSZleF-i7jT
Keyboard control
----------------
Bindings inherited from mpv
^^^^^^^^^^^^^^^^^^^^^^^^^^^
For convenience purpose, I try to mimic **mpv** default keybindings, but many
are slightly different from **mpv** exact behaviour (mainly because of the lack
of keys which are unsupported by ``curses``). So I will list all of them here
for you to `compare <https://github.com/mpv-player/mpv/blob/master/DOCS/man/mpv.rst#keyboard-control>`_:
Left and Right
Seek backward/forward 5 seconds. Shifted arrow does a 1 second seek.
Up and Down
Seek backward/forward 1 minute.
``[`` and ``]``
Decrease/increase current playback speed by 10%.
``{`` and ``}``
Halve/double current playback speed.
Backspace
Reset playback speed to normal.
``<`` and ``>``
Go backward/forward in the playlist.
Return
Start playing.
Space / ``p``
Pause (pressing again unpauses).
``.``
Step forward. Pressing once will pause, every consecutive press will play
one frame and then go into pause mode again.
``,``
Step backward. Pressing once will pause, every consecutive press will play
one frame in reverse and then go into pause mode again.
``q``
Stop playing and quit.
``/`` / ``9`` and ``*`` / ``0``
Decrease/increase volume.
``m``
Mute sound.
``_``
Cycle through the available video tracks.
``#``
Cycle through the available audio tracks.
``f``
Toggle fullscreen.
``T``
Toggle stay-on-top.
``w`` and ``e``
Decrease/increase pan-and-scan range.
``o`` / ``P``
Show progression bar, elapsed time and total duration on the OSD.
``O``
Toggle OSD states between normal and playback time/duration.
``v``
Toggle subtitle visibility.
``j`` and ``J``
Cycle through the available subtitles.
``x`` and ``z``
Adjust subtitle delay by +/- 0.1 seconds.
``l``
Set/clear A-B loop points.
``L``
Toggle infinite looping.
Ctrl-``+`` and Ctrl-``-``
Adjust audio delay (A/V sync) by +/- 0.1 seconds.
``u``
Switch between applying no style overrides to SSA/ASS subtitles, and
overriding them almost completely with the normal subtitle style.
``V``
Toggle subtitle VSFilter aspect compatibility mode.
``r`` and ``t``
Move subtitles up/down.
``s``
Take a screenshot.
``S``
Take a screenshot, without subtitles.
Alt-``s``
Take a screenshot each frame.
Page Up and Page Down
Seek to the beginning of the previous/next chapter.
``d``
Activate/deactivate deinterlacer.
``A``
Cycle aspect ratio override.
``1`` and ``2``
Adjust contrast.
``3`` and ``4``
Adjust brightness.
``5`` and ``6``
Adjust gamma.
``7`` and ``8``
Adjust saturation.
Alt-``0``
Resize video window to half its original size.
Alt-``1``
Resize video window to its original size.
Alt-``2``
Resize video window to double its original size.
``E``
Cycle through editions.
Movements and selections
^^^^^^^^^^^^^^^^^^^^^^^^
The following keybindings are Emacs-like since most characters are taken by
**mpv**.
Ctrl-``p`` and Ctrl-``n``
Move a single line up/down.
Alt-``v`` and Ctrl-``v``
Move a single page up/down.
Home / Ctrl-``<`` and End / Ctrl-``>``
Move to the beginning/end of the playlist.
Ctrl-Space
Deselect/reselect the current entry and move down a line.
Playlist manipulation
^^^^^^^^^^^^^^^^^^^^^
Ctrl-``o``
Open playlist.
Ctrl-``i``
Insert playlist.
Ctrl-``f`` and Alt-``f``
Search forward/backward for a pattern.
Alt-``m``
Cycle through playing modes.
Delete
Delete the current entry.
``W``
Save the current playlist under JSON format.
F5
Redraw the screen content.
``:``
Execute a **mpv** command.
Configuration files
-------------------
If not specified by the ``--config``, (user-specific) configuration file is
``~/.config/comp/settings.ini``. Default configurations
are listed below::
[comp]
# Initial playing mode, which can be one of these 8 modes: play-current,
# play-all, play-selected, repeat-current, repeat-all, repeat-selected,
# shuffle-all and shuffle-selected.
play-mode = play-current
[mpv]
# Options to be parsed to mpv. See OPTIONS section on mpv(1) man pages for
# its complete list of available options.
# For example:
#vo = xv
#ontop = yes
#border = no
#force-window = yes
#autofit = 500x280
#geometry = -15-50
[youtube-dl]
# Video format/quality to be passed to youtube-dl. See FORMAT SELECTION in
# youtube-dl(1) man page for more details and descriptions.
format = bestvideo+bestaudio
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.

535
comp Executable file
View File

@ -0,0 +1,535 @@
#!/usr/bin/env python3
# comp - Curses Omni Media Player
#
# comp 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.
#
# comp 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 comp. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright (C) 2017 Nguyễn Gia Phong <vn.mcsinyx@gmail.com>
__version__ = '0.4.6'
import curses
import re
from argparse import ArgumentParser
from collections import deque
from configparser import ConfigParser
from curses.ascii import ctrl, alt
from functools import reduce
from gettext import bindtextdomain, gettext as _, textdomain
from os.path import expanduser
from threading import Thread
from traceback import print_exception
from mpv import MPV
from pkg_resources import resource_filename
from omp import extract_info, Omp
# Init gettext
bindtextdomain('omp', resource_filename('omp', 'locale'))
textdomain('omp')
# Global constants
SYSTEM_CONFIG = '/etc/comp/settings.ini'
USER_CONFIG = expanduser('~/.config/comp/settings.ini')
MODES = ("play-current", "play-all", "play-selected", "repeat-current",
"repeat-all", "repeat-selected", "shuffle-all", "shuffle-selected")
MODE_STR_LEN = max(len(_(mode)) for mode in MODES)
DURATION_COL_LEN = max(len(_("Duration")), 8)
def justified(s, width):
"""Return s left-justified of length width."""
return s.ljust(width)[:width]
class Comp(Omp):
"""Meta object for drawing and playing.
Attributes:
entries (list): list of all tracks
json_file (str): path to save JSON playlist
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
search_str (str): regex search string
scr (curses WindowObject): curses window object
start (int): index of the first track to be printed on screen
y (int): the current y-coordinate
"""
def __new__(cls, entries, json_file, mode, mpv_args, ytdlf):
self = object.__new__(cls)
self.play_backward, self.reading = False, False
self.playing, self.start, self.y = -1, 0, 1
self.json_file, self.mode = json_file, mode
self.entries, self.played = entries, []
self.playlist, self.search_str = iter(()), ''
self.mp = MPV(input_default_bindings=True, input_vo_keyboard=True,
ytdl=True, ytdl_format=ytdlf)
self.scr = curses.initscr()
return self
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
curses.update_lines_cols()
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)
def update_status(self, message='', msgattr=curses.A_NORMAL):
"""Update the status lines at the bottom of the screen."""
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)
if self.mp.osd.duration is not None:
self.played[self.playing]['duration'] = self.mp.osd.duration
add_status_str(':', X=5, lpad=3)
if self.mp.video: add_status_str('V', x=1, X=2)
if self.mp.audio: add_status_str('A', X=1)
add_status_str(self.mp.osd.time_pos or '00:00:00', x=4, X=12)
add_status_str('/', x=13, X=14)
add_status_str(self.mp.osd.duration or '00:00:00', x=15, X=23)
add_status_str('|' if self.mp.pause else '>', x=24, X=25)
add_status_str(self.mp.media_title or '', x=26,
attr=curses.color_pair(12)|curses.A_BOLD)
add_status_str(_(self.mode), x=-2-len(_(self.mode)))
self.scr.refresh()
def print_msg(self, message, error=False):
"""Print the given message, in red is it's an error."""
attributes = curses.color_pair(1) if error else curses.A_NORMAL
self.adds(message, curses.LINES-1, attr=attributes, lpad=0)
self.scr.refresh()
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
def play(self, force=False):
"""Play the next track."""
def mpv_play(entry, force):
self.setno('playing')
entry['playing'] = True
try:
self.mp.play(entry['filename'])
except:
entry['error'] = True
self.print(entry)
if force: self.mp.pause = False
self.mp.wait_for_playback()
self.play()
entry['playing'] = False
self.print(entry)
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()
def _writeln(self, y, title, duration, attr):
title_len = curses.COLS - DURATION_COL_LEN - 3
self.adds(title, y, attr=attr)
self.adds(duration, y, x=title_len+1, attr=attr)
self.scr.refresh()
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
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:
self._writeln(y, entry['title'], entry['duration'],
curses.color_pair(color) | curses.A_BOLD)
else:
self._writeln(y, entry['title'], entry['duration'],
curses.A_NORMAL)
def refresh(self):
"""Redraw the whole screen."""
self._writeln(0, _("Title"), _("Duration"),
curses.color_pair(10) | curses.A_BOLD)
for i, entry in enumerate(self.entries[self.start:][:curses.LINES-3]):
self.print(entry, i + 1)
self.scr.clrtobot()
self.update_status()
def property_handler(self, name, val): self.update_status()
def __init__(self, entries, json_file, mode, mpv_args, ytdlf):
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)
Omp.__init__(self, entries, json_file, mode, mpv_args, ytdlf)
self.refresh()
def __enter__(self): return self
def read_input(self, prompt):
"""Print the prompt string at the bottom of the screen then read
from standard input.
"""
self.adds(prompt, curses.LINES - 1, lpad=0)
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 move(self, delta):
"""Move to the relatively next delta entry."""
if not (self.entries and delta): return
start, prev_entry = self.start, self.current()
maxy = min(len(self.entries), curses.LINES - 3)
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
else:
self.y += delta
if self.start == start:
self.print(prev_entry)
self.print()
else:
self.refresh()
def search(self, backward=False):
"""Prompt then search for a pattern."""
s = self.read_input(_("Search {}ward [{{}}]: ".format(
'back' if backward else 'for')).format(self.search_str))
if s: self.search_str = s
pattern = re.compile(self.search_str, re.IGNORECASE)
entries = deque(self.entries)
if backward:
entries.rotate(-self.idx())
entries.reverse()
else:
entries.rotate(-self.idx() - 1)
for entry in entries:
if pattern.search(entry['title']) is not None:
self.move(self.idx(entry) - self.idx())
return
self.print_msg(_("'{}' not found").format(self.search_str), error=True)
def resize(self):
curses.update_lines_cols()
self.scr.clear()
l = curses.LINES - 3
if curses.COLS < MODE_STR_LEN + 42 or l < 1: # too small
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])
self.scr.refresh()
elif self.y > l: # shorter than the current entry
self.start += self.y - l
self.y = l
self.refresh()
elif 0 < self.start > len(self.entries) - l: # longer than the list
idx, self.start = self.idx(), min(0, len(self.entries) - l)
self.y = idx - self.start + 1
if self.y > l:
self.start += self.y - l
self.y = l
self.refresh()
else:
self.refresh()
def __exit__(self, exc_type, exc_value, traceback):
curses.nocbreak()
self.scr.keypad(False)
curses.echo()
curses.endwin()
Omp.__exit__(self, exc_type, exc_value, traceback)
if exc_value is not None:
print_exception(exc_type, exc_value, traceback)
parser = ArgumentParser(description='Curses Omni Media Player')
parser.add_argument('-v', '--version', action='version',
version='%(prog)s {}'.format(__version__))
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('playlist', help='path or URL to the playlist')
parser.add_argument('-c', '--config', default=USER_CONFIG, required=False,
help='path to the configuration file')
parser.add_argument('--vo', required=False, metavar='DRIVER',
help='specify the video output backend to be used. See\
VIDEO OUTPUT DRIVERS in mpv(1) 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')
args = parser.parse_args()
entries = extract_info(args.playlist, args.extractor)
if entries is None:
print(_("'{}': Can't extract playlist").format(args.playlist))
exit()
json_file = args.playlist if args.extractor == 'json' else ''
config = ConfigParser()
config.read(args.config)
mode = config.get('comp', 'play-mode', fallback='play-current')
mpv_args = dict(config['mpv']) if 'mpv' in config else {}
if args.vo is not None: mpv_args['vo'] = args.vo
ytdlf = args.format or config.get('youtube-dl', 'format',
fallback='bestvideo+bestaudio')
with Comp(entries, json_file, mode, mpv_args, ytdlf) as comp:
while True:
c = comp.scr.get_wch()
comp.print_msg('')
# mpv keybindings
if c == curses.KEY_LEFT:
comp.seek(-5, precision='exact')
elif c == curses.KEY_RIGHT:
comp.seek(5, precision='exact')
elif c == curses.KEY_SLEFT: # Shifted Left-arrow
comp.seek(-1, precision='exact')
elif c == curses.KEY_SRIGHT: # Shifted Right-arrow
comp.seek(1, precision='exact')
elif c == curses.KEY_UP:
comp.seek(-60, precision='exact')
elif c == curses.KEY_DOWN:
comp.seek(60, precision='exact')
elif c == curses.KEY_PPAGE:
comp.add('chapter', 1)
elif c == curses.KEY_NPAGE:
comp.add('chapter', -1)
elif c == '[':
comp.multiply('speed', 0.9091)
elif c == ']':
comp.multiply('speed', 1.1)
elif c == '{':
comp.multiply('speed', 0.5)
elif c == '}':
comp.multiply('speed', 2.0)
elif c == curses.KEY_BACKSPACE:
comp.mp.speed = 1.0
elif c == 'q':
comp.print_msg(_("Save playlist? [Y/n]"))
if comp.scr.get_wch() not in _("Nn"): comp.dump_json()
break
elif c in ('p', ' '):
comp.cycle('pause')
elif c == '.':
comp.mp.frame_step()
elif c == ',':
comp.mp.frame_back_step()
elif c == '<':
try:
if comp.mp.time_pos < 1:
comp.next(backward=True)
else:
comp.seek(0, 'absolute')
except:
pass
elif c == '>':
comp.next()
elif c == '\n': # curses.KEY_ENTER doesn't work
comp.update_playlist()
comp.next(force=True)
elif c == 'O':
comp.mp.command('cycle-values', 'osd-level', 3, 1)
elif c in ('o', 'P'):
comp.mp.show_progress()
elif c == 'z':
comp.add('sub-delay', -0.1)
elif c == 'x':
comp.add('sub-delay', 0.1)
elif c == ctrl('+'):
comp.add('audio-delay', 0.1)
elif c == ctrl('-'):
comp.add('audio-delay', -0.1)
elif c in ('/', '9'):
comp.add('volume', -2)
elif c in ('*', '0'):
comp.add('volume', 2)
elif c == 'm':
comp.cycle('mute')
elif c == '1':
comp.add('contrast', -1)
elif c == '2':
comp.add('contrast', 1)
elif c == '3':
comp.add('brightness', -1)
elif c == '4':
comp.add('brightness', 1)
elif c == '5':
comp.add('gamma', -1)
elif c == '6':
comp.add('gamma', 1)
elif c == '7':
comp.add('saturation', -1)
elif c == '8':
comp.add('saturation', 1)
elif c == alt('0'):
comp.mp.window_scale = 0.5
elif c == alt('1'):
comp.mp.window_scale = 1.0
elif c == alt('2'):
comp.mp.window_scale = 2.0
elif c == 'd':
comp.cycle('deinterlace')
elif c == 'r':
comp.add('sub-pos', -1)
elif c == 't':
comp.add('sub-pos', 1)
elif c == 'v':
comp.cycle('sub-visibility')
elif c == 'V':
comp.cycle('sub-ass-vsfilter-aspect-compat')
elif c == 'u':
comp.mp.command('cycle-values', 'sub-ass-override', 'force', 'no')
elif c == 'j':
comp.cycle('sub', 'up')
elif c == 'J':
comp.cycle('sub', 'down')
elif c == '#':
comp.cycle('audio')
elif c == '_':
comp.cycle('video')
elif c == 'T':
comp.cycle('ontop')
elif c == 'f':
comp.cycle('fullscreen')
elif c == 's':
comp.mp.screenshot()
elif c == 'S':
comp.mp.screenshot(includes='')
elif c == alt('s'):
comp.mp.screenshot(mode='each-frame')
elif c == 'w':
comp.add('panscan', -0.1)
elif c == 'e':
comp.add('panscan', 0.1)
elif c == 'A':
comp.mp.command('cycle-values', 'video-aspect',
'16:9', '4:3', '2.35:1', '-1')
elif c == 'E':
comp.cycle('edition')
elif c == 'l':
comp.mp.command('ab-loop')
elif c == 'L':
comp.mp.command('cycle-values', 'loop-file', 'inf', 'no')
# Emacs keybindings
elif c == ctrl('p'):
comp.move(-1)
elif c == ctrl('n'):
comp.move(1)
elif c == alt('v'):
comp.move(4 - curses.LINES)
elif c == ctrl('v'):
comp.move(curses.LINES - 4)
elif c in (ctrl('<'), curses.KEY_HOME):
comp.move(-len(comp.entries))
elif c in (ctrl('>'), curses.KEY_END):
comp.move(len(comp.entries))
elif c == ctrl(' '):
comp.current()['selected'] = not comp.current().get('selected')
comp.move(1)
elif c == ctrl('o'):
extractor = comp.read_input(_("Playlist extractor: "))
filename = comp.read_input(_("Open: "))
entries = extract_info(filename, extractor)
if entries is None:
comp.print_msg(
_("'{}': Can't extract playlist").format(filename))
else:
comp.entries, comp.start, comp.y = entries, 0, 1
comp.refresh()
elif c == ctrl('i'):
extractor = comp.read_input(_("Playlist extractor: "))
filename = comp.read_input(_("Insert: "))
entries = extract_info(filename, extractor)
if entries is None:
comp.print_msg(
_("'{}': 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.refresh()
elif c == ctrl('f'):
comp.search()
elif c == alt('f'):
comp.search(backward=True)
elif c == alt('m'):
comp.mode = MODES[(MODES.index(comp.mode) + 1) % 8]
comp.update_status()
elif c == curses.KEY_DC:
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
comp.refresh()
elif c == 'W':
comp.dump_json()
elif c in (curses.KEY_F5, curses.KEY_RESIZE):
comp.resize()
elif c == ':':
try:
comp.mp.command(*comp.read_input(':').split())
except:
comp.print_msg(_("Failed to execute command"), error=True)

261
doc/comp.1 Normal file
View File

@ -0,0 +1,261 @@
.\" Process this file with
.\" groff -man -Tutf8 comp.1
.\"
.TH COMP 1 2018-01-25 comp
.SH NAME
comp \- Curses Omni Media Player
.SH SYNOPSIS
\fBcomp\fR [\fB-h\fR] [\fB-v\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
.BR curses (3).
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 -v, --version
show program's version number 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
.SS Bindings inherited from mpv
For convenience purpose, I try to mimic
.BR mpv (1)
default keybindings, but many are slightly different from
.BR mpv (1)
exact behaviour (mainly because of the lack of keys which are unsupported by
.BR curses (3)).
So I will list all of them here for you to compare:
.TP
.B Left and Right
Seek backward/forward 5 seconds. Shifted arrow does a 1 second seek.
.TP
.B Up and Down
Seek backward/forward 1 minute.
.TP
.B [ and ]
Decrease/increase current playback speed by 10%.
.TP
.B { and }
Halve/double current playback speed.
.TP
.B Backspace
Reset playback speed to normal.
.TP
.B < and >
Go backward/forward in the playlist.
.TP
.B Return
Start playing.
.TP
.B Space / p
Pause (pressing again unpauses).
.TP
.B .
Step forward. Pressing once will pause, every consecutive press will play
one frame and then go into pause mode again.
.TP
.B ,
Step backward. Pressing once will pause, every consecutive press will play
one frame in reverse and then go into pause mode again.
.TP
.B q
Stop playing and quit.
.TP
.B / and *
Decrease/increase volume.
.TP
.B 9 and 0
Decrease/increase volume.
.TP
.B m
Mute sound.
.TP
.B _
Cycle through the available video tracks.
.TP
.B #
Cycle through the available audio tracks.
.TP
.B f
Toggle fullscreen.
.TP
.B T
Toggle stay-on-top.
.TP
.B w and e
Decrease/increase pan-and-scan range.
.TP
.B o or P
Show progression bar, elapsed time and total duration on the OSD.
.TP
.B O
Toggle OSD states between normal and playback time/duration.
.TP
.B v
Toggle subtitle visibility.
.TP
.B j and J
Cycle through the available subtitles.
.TP
.B x and z
Adjust subtitle delay by +/- 0.1 seconds.
.TP
.B l
Set/clear A-B loop points.
.TP
.B L
Toggle infinite looping.
.TP
.B Ctrl-+ and Ctrl--
Adjust audio delay (A/V sync) by +/- 0.1 seconds.
.TP
.B u
Switch between applying no style overrides to SSA/ASS subtitles, and
overriding them almost completely with the normal subtitle style.
.TP
.B V
Toggle subtitle VSFilter aspect compatibility mode.
.TP
.B r and t
Move subtitles up/down.
.TP
.B s
Take a screenshot.
.TP
.B S
Take a screenshot, without subtitles.
.TP
.B Alt-s
Take a screenshot each frame.
.TP
.B Page Up and Page Down
Seek to the beginning of the previous/next chapter.
.TP
.B d
Activate/deactivate deinterlacer.
.TP
.B A
Cycle aspect ratio override.
.TP
.B 1 and 2
Adjust contrast.
.TP
.B 3 and 4
Adjust brightness.
.TP
.B 5 and 6
Adjust gamma.
.TP
.B 7 and 8
Adjust saturation.
.TP
.B Alt-0
Resize video window to half its original size.
.TP
.B Alt-1
Resize video window to its original size.
.TP
.B Alt-2
Resize video window to double its original size.
.TP
.B E
Cycle through editions.
.SS Movements and selections
The following keybindings are Emacs-like since most characters are taken by
.BR mpv (1).
.TP
.B Ctrl-p and Ctrl-n
Move a single line up/down.
.TP
.B Alt-v and Ctrl-v
Move a single page up/down.
.TP
.B Ctrl-< and Ctrl->
Move to the beginning/end of the playlist.
.TP
.B Home and End
Move to the beginning/end of the playlist.
.TP
.B Ctrl-Space
Deselect/reselect the current entry and move down a line.
.SS Playlist manipulation
.TP
.B Ctrl-o
Open playlist.
.TP
.B Ctrl-i
Insert playlist.
.TP
.B Ctrl-f and Alt-f
Search forward/backward for a pattern.
.TP
.B Alt-m
Cycle through playing modes.
.TP
.B Delete
Delete the current entry.
.TP
.B W
Save the current playlist under JSON format.
.TP
.B F5
Redraw the screen content.
.TP
.B :
Execute a mpv command.
.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/watch?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)

BIN
doc/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

View File

@ -0,0 +1,84 @@
# Vietnamese translation for Omp front-ends.
# Copyright (C) 2018 Nguyễn Gia Phong
# Nguyễn Gia Phong <vn.mcsinyx@gmail.com>, 2018
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: 2017-04-05 11:00+0700\n"
"PO-Revision-Date: 2017-04-06 22:29+0700\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: pygettext.py 1.5\n"
"X-Generator: Poedit 1.8.11\n"
"Last-Translator: \n"
"Plural-Forms: nplurals=1; plural=0;\n"
"Language: vi_VN\n"
msgid "Curses Omni Media Player"
msgstr "Phần mềm phát đa phương tiện sử dụng thư viện curses"
msgid "play-current"
msgstr "chơi-một"
msgid "play-all"
msgstr "chơi-tất-cả"
msgid "play-selected"
msgstr "chơi-đã-chọn"
msgid "repeat-current"
msgstr "lặp-một"
msgid "repeat-all"
msgstr "lặp-tất-cả"
msgid "repeat-selected"
msgstr "lặp-đã-chọn"
msgid "shuffle-all"
msgstr "ngẫu-nhiên-tất-cả"
msgid "shuffle-selected"
msgstr "ngẫu-nhiên-đã-chọn"
msgid "Title"
msgstr "Tiêu đề"
msgid "Current size: {}x{}. Minimum size: {}x4."
msgstr "Kích thước hiện tại: {}x{}. Kích thước tối thiểu: {}x4."
msgid "Save playlist? [Y/n]"
msgstr "Lưu danh sách phát? [C/k]"
msgid "Nn"
msgstr "Kk"
msgid "Save playlist to [{}]: "
msgstr "Lưu danh sách phát tại [{}]: "
msgid "'{}': Can't open file for writing"
msgstr "'{}': Không mở được tệp để ghi"
msgid "'{}' written"
msgstr "'{}' đã ghi"
msgid "Duration"
msgstr "Thời lượng"
msgid "Pattern not found"
msgstr "Không tìm thấy mẫu (pattern)"
msgid "'{}': Can't extract playlist"
msgstr "'{}': Không mở được danh sách phát"
msgid "Playlist extractor: "
msgstr "Công cụ mở danh sách phát: "
msgid "Insert: "
msgstr "Chèn: "
msgid "Open: "
msgstr "Mở: "

594
mpv.py
View File

@ -1,594 +0,0 @@
from ctypes import *
import threading
import os
import asyncio
# vim: ts=4 sw=4
backend = CDLL('libmpv.so')
class MpvHandle(c_void_p):
pass
class ErrorCode:
""" For documentation on these, see mpv's libmpv/client.h """
SUCCESS = 0
EVENT_QUEUE_FULL = -1
NOMEM = -2
UNINITIALIZED = -3
INVALID_PARAMETER = -4
OPTION_NOT_FOUND = -5
OPTION_FORMAT = -6
OPTION_ERROR = -7
PROPERTY_NOT_FOUND = -8
PROPERTY_FORMAT = -9
PROPERTY_UNAVAILABLE = -10
PROPERTY_ERROR = -11
COMMAND = -12
EXCEPTION_DICT = {
0: None,
-1: lambda *a: MemoryError('mpv event queue full', *a),
-2: lambda *a: MemoryError('mpv cannot allocate memory', *a),
-3: lambda *a: ValueError('Uninitialized mpv handle used', *a),
-4: lambda *a: ValueError('Invalid value for mpv parameter', *a),
-5: lambda *a: AttributeError('mpv option does not exist', *a),
-6: lambda *a: TypeError('Tried to set mpv option using wrong format', *a),
-7: lambda *a: ValueError('Invalid value for mpv option', *a),
-8: lambda *a: AttributeError('mpv property does not exist', *a),
-9: lambda *a: TypeError('Tried to set mpv property using wrong format', *a),
-10: lambda *a: AttributeError('mpv property is not available', *a),
-11: lambda *a: ValueError('Invalid value for mpv property', *a),
-12: lambda *a: SystemError('Error running mpv command', *a)
}
@classmethod
def raise_for_ec(kls, func, *args):
ec = kls.EXCEPTION_DICT[func(*args)]
if ec:
raise ec(*args)
class MpvFormat(c_int):
NONE = 0
STRING = 1
OSD_STRING = 2
FLAG = 3
INT64 = 4
DOUBLE = 5
NODE = 6
NODE_ARRAY = 7
NODE_MAP = 8
class MpvEventID(c_int):
NONE = 0
SHUTDOWN = 1
LOG_MESSAGE = 2
GET_PROPERTY_REPLY = 3
SET_PROPERTY_REPLY = 4
COMMAND_REPLY = 5
START_FILE = 6
END_FILE = 7
FILE_LOADED = 8
TRACKS_CHANGED = 9
TRACK_SWITCHED = 10
IDLE = 11
PAUSE = 12
UNPAUSE = 13
TICK = 14
SCRIPT_INPUT_DISPATCH = 15
CLIENT_MESSAGE = 16
VIDEO_RECONFIG = 17
AUDIO_RECONFIG = 18
METADATA_UPDATE = 19
SEEK = 20
PLAYBACK_RESTART = 21
PROPERTY_CHANGE = 22
CHAPTER_CHANGE = 23
ANY = ( SHUTDOWN, LOG_MESSAGE, GET_PROPERTY_REPLY, SET_PROPERTY_REPLY, COMMAND_REPLY, START_FILE, END_FILE,
FILE_LOADED, TRACKS_CHANGED, TRACK_SWITCHED, IDLE, PAUSE, UNPAUSE, TICK, SCRIPT_INPUT_DISPATCH,
CLIENT_MESSAGE, VIDEO_RECONFIG, AUDIO_RECONFIG, METADATA_UPDATE, SEEK, PLAYBACK_RESTART, PROPERTY_CHANGE,
CHAPTER_CHANGE )
class MpvEvent(Structure):
_fields_ = [('event_id', MpvEventID),
('error', c_int),
('reply_userdata', c_ulonglong),
('data', c_void_p)]
def as_dict(self):
dtype = {MpvEventID.END_FILE: MpvEventEndFile,
MpvEventID.PROPERTY_CHANGE: MpvEventProperty,
MpvEventID.GET_PROPERTY_REPLY: MpvEventProperty,
MpvEventID.LOG_MESSAGE: MpvEventLogMessage,
MpvEventID.SCRIPT_INPUT_DISPATCH: MpvEventScriptInputDispatch,
MpvEventID.CLIENT_MESSAGE: MpvEventClientMessage
}.get(self.event_id.value, None)
return {'event_id': self.event_id.value,
'error': self.error,
'reply_userdata': self.reply_userdata,
'event': cast(self.data, POINTER(dtype)).contents.as_dict() if dtype else None}
class MpvEventProperty(Structure):
_fields_ = [('name', c_char_p),
('format', MpvFormat),
('data', c_void_p)]
def as_dict():
pass # FIXME
class MpvEventLogMessage(Structure):
_fields_ = [('prefix', c_char_p),
('level', c_char_p),
('text', c_char_p)]
def as_dict(self):
return { name: getattr(self, name).value for name, _t in _fields_ }
class MpvEventEndFile(c_int):
EOF_OR_INIT_FAILURE = 0
RESTARTED = 1
ABORTED = 2
QUIT = 3
def as_dict(self):
return {'reason': self.value}
class MpvEventScriptInputDispatch(Structure):
_fields_ = [('arg0', c_int),
('type', c_char_p)]
def as_dict(self):
pass # TODO
class MpvEventClientMessage(Structure):
_fields_ = [('num_args', c_int),
('args', POINTER(c_char_p))]
def as_dict(self):
return { 'args': [ self.args[i].value for i in range(self.num_args.value) ] }
WakeupCallback = CFUNCTYPE(None, c_void_p)
def _handle_func(name, args=[], res=None):
func = getattr(backend, name)
if res is not None:
func.restype = res
func.argtypes = [MpvHandle] + args
def wrapper(*args):
if res is not None:
return func(*args)
else:
ErrorCode.raise_for_ec(func, *args)
globals()['_'+name] = wrapper
backend.mpv_client_api_version.restype = c_ulong
def _mpv_client_api_version():
ver = backend.mpv_client_api_version()
return ver>>16, ver&0xFFFF
backend.mpv_free.argtypes = [c_void_p]
_mpv_free = backend.mpv_free
backend.mpv_create.restype = MpvHandle
_mpv_create = backend.mpv_create
_handle_func('mpv_client_name', [], c_char_p)
_handle_func('mpv_initialize')
_handle_func('mpv_detach_destroy', [], c_int)
_handle_func('mpv_terminate_destroy', [], c_int)
_handle_func('mpv_load_config_file', [c_char_p])
_handle_func('mpv_suspend', [], c_int)
_handle_func('mpv_resume', [], c_int)
_handle_func('mpv_get_time_us', [], c_ulonglong)
_handle_func('mpv_set_option', [c_char_p, MpvFormat, c_void_p])
_handle_func('mpv_set_option_string', [c_char_p, c_char_p])
_handle_func('mpv_command', [POINTER(c_char_p)])
_handle_func('mpv_command_string', [c_char_p, c_char_p])
_handle_func('mpv_command_async', [c_ulonglong, POINTER(c_char_p)])
_handle_func('mpv_set_property', [c_char_p, MpvFormat, c_void_p])
_handle_func('mpv_set_property_string', [c_char_p, c_char_p])
_handle_func('mpv_set_property_async', [c_ulonglong, c_char_p, MpvFormat, c_void_p])
_handle_func('mpv_get_property', [c_char_p, MpvFormat, c_void_p])
_handle_func('mpv_get_property_string', [c_char_p], c_char_p)
_handle_func('mpv_get_property_osd_string', [c_char_p], c_char_p)
_handle_func('mpv_get_property_async', [c_ulonglong, c_char_p, MpvFormat])
_handle_func('mpv_observe_property', [c_ulonglong, c_char_p, MpvFormat])
_handle_func('mpv_unobserve_property', [c_ulonglong])
backend.mpv_event_name.restype = c_char_p
backend.mpv_event_name.argtypes = [c_int]
_mpv_event_name = backend.mpv_event_name
_handle_func('mpv_request_event', [MpvEventID, c_int])
_handle_func('mpv_request_log_messages', [c_char_p])
_handle_func('mpv_wait_event', [c_double], POINTER(MpvEvent))
_handle_func('mpv_wakeup', [], c_int)
_handle_func('mpv_set_wakeup_callback', [WakeupCallback, c_void_p], c_int)
_handle_func('mpv_get_wakeup_pipe', [], c_int)
class ynbool:
def __init__(self, val=False):
if not val or val == b'no' or val == 'no':
self.val = False
else:
self.val = True
def __nonzero__(self):
return self.val
def __str__(self):
return 'yes' if self.val else 'no'
def __repr__(self):
return str(self.val)
def _ensure_encoding(possibly_bytes):
return possibly_bytes.decode('utf8') if type(possibly_bytes) is bytes else possibly_bytes
def _event_generator(handle):
while True:
event = _mpv_wait_event(handle, 0).contents
if event.event_id.value == MpvEventID.NONE:
raise StopIteration()
yield event
class MPV:
""" See man mpv(1) for the details of the implemented commands. """
def __init__(self, loop=None, **kwargs):
self.handle = _mpv_create()
self.event_callbacks = []
self._event_fd = _mpv_get_wakeup_pipe(self.handle)
self._playback_cond = threading.Condition()
def mpv_event_extractor():
os.read(self._event_fd, 512)
for event in _event_generator(self.handle):
devent = event.as_dict() # copy data from ctypes
if devent['event_id'] in (MpvEventID.SHUTDOWN, MpvEventID.END_FILE, MpvEventID.PAUSE):
with self._playback_cond:
self._playback_cond.notify_all()
for callback in self.event_callbacks:
callback.call()
if loop:
loop.add_reader(self._event_fd, mpv_event_extractor)
else:
def loop_runner():
loop = asyncio.new_event_loop()
loop.add_reader(self._event_fd, mpv_event_extractor)
try:
loop.run_forever()
finally:
loop.close()
self._event_thread = threading.Thread(target=loop_runner, daemon=True)
self._event_thread.start()
_mpv_set_option_string(self.handle, b'audio-display', b'no')
istr = lambda o: ('yes' if o else 'no') if type(o) is bool else str(o)
for k,v in kwargs.items():
_mpv_set_option_string(self.handle, k.replace('_', '-').encode('utf8'), istr(v).encode('utf8'))
_mpv_initialize(self.handle)
def wait_for_playback(self):
with self._playback_cond:
self._playback_cond.wait()
# def __del__(self):
# _mpv_terminate_destroy(self.handle)
def command(self, name, *args):
args = [name.encode('utf8')] + [ str(arg).encode('utf8') for arg in args if arg is not None ] + [None]
_mpv_command(self.handle, (c_char_p*len(args))(*args))
def seek(self, amount, reference="relative", precision="default-precise"):
assert reference in ('relative', 'absolute', 'absolute-percent')
assert precision in ('default-precise', 'exact', 'keyframes')
self.command('seek', amount, reference, precision)
def revert_seek(self):
self.command('revert_seek');
def frame_step(self):
self.command('frame_step')
def frame_back_step(self):
self.command('frame_back_step')
def _set_property(self, name, value):
self.command('set_property', name, str(value))
def _add_property(self, name, value=None):
self.command('add_property', name, value)
def _cycle_property(self, name, direction='up'):
self.command('cycle_property', name, direction)
def _multiply_property(self, name, factor):
self.command('multiply_property', name, factor)
def screenshot(self, includes='subtitles', mode='single'):
assert includes in ('subtitles', 'video', 'window')
assert mode in ('single', 'each-frame')
self.command('screenshot', includes, mode)
def screenshot_to_file(self, filename, includes='subtitles'):
assert includes in ('subtitles', 'video', 'window')
self.command('screenshot_to_file', filename, includes)
def playlist_next(self, mode='weak'):
assert mode in ('weak', 'force')
self.command('playlist_next', mode)
def playlist_prev(self, mode='weak'):
assert mode in ('weak', 'force')
self.command('playlist_prev', mode)
def loadfile(self, filename, mode='replace'):
assert mode in ('replace', 'append', 'append-play')
self.command('loadfile', filename, mode)
def loadlist(self, playlist, mode='replace'):
self.command('loadlist', playlist, mode)
def playlist_clear(self):
self.command('playlist_clear')
def playlist_remove(self, index='current'):
self.command('playlist_remove', index)
def playlist_move(self, index1, index2):
self.command('playlist_move', index1, index2)
def run(self, command, *args):
self.command('run', command, *args)
def quit(self, code=None):
self.command('quit', code)
def quit_watch_later(self, code=None):
self.command('quit_watch_later', code)
def sub_add(self, filename):
self.command('sub_add', filename)
def sub_remove(self, sub_id=None):
self.command('sub_remove', sub_id)
def sub_reload(self, sub_id=None):
self.command('sub_reload', sub_id)
def sub_step(self, skip):
self.command('sub_step', skip)
def sub_seek(self, skip):
self.command('sub_seek', skip)
def toggle_osd(self):
self.command('osd')
def show_text(self, string, duration='-', level=None):
self.command('show_text', string, duration, level)
def show_progress(self):
self.command('show_progress')
def discnav(self, command):
assert command in ('up', 'up', 'down', 'down', 'left', 'right', 'left', 'right', 'menu', 'select',
'prev', 'mouse', 'mouse_move')
self.command('discnav', command)
def write_watch_later_config(self):
self.command('write_watch_later_config')
def overlay_add(self, overlay_id, x, y, file_or_fd, offset, fmt, w, h, stride):
self.command('overlay_add', overlay_id, x, y, file_or_fd, offset, fmt, w, h, stride)
def overlay_remove(self, overlay_id):
self.command('overlay_remove', overlay_id)
def script_message(self, *args):
self.command('script_message', *args)
def script_message_to(self, target, *args):
self.command('script_message_to', target, *args)
@property
def metadata(self):
...
def chapter_metadata(self):
...
def vf_metadata(self):
...
# Convenience functions
def play(self, filename):
self.command('loadfile', filename)
# Complex properties
_VIDEO_PARAMS_LIST = (
('pixelformat', str),
('w', int),
('h', int),
('dw', int),
('dh', int),
('aspect', float),
('par', float),
('colormatrix', str),
('colorlevels', str),
('chroma-location', str),
('rotate', int))
@property
def video_params(self):
return self._get_dict('video-params/', _VIDEO_PARAMS_LIST)
@property
def video_out_params(self):
return self._get_dict('video-out-params/', _VIDEO_PARAMS_LIST)
@property
def playlist(self):
return self._get_list('playlist/', (('filename', str),))
@property
def track_list(self):
return self._get_list('track-list/', (
('id', int),
('type', str),
('src-id', int),
('title', str),
('lang', str),
('albumart', ynbool),
('default', ynbool),
('external', ynbool),
('external-filename', str),
('codec', str),
('selected', ynbool)))
@property
def chapter_list(self):
return self._get_dict('chapter-list/', (('title', str), ('time', float)))
def _get_dict(self, prefix, props):
return { name: proptype(_ensure_encoding(_mpv_get_property_string(self.handle, (prefix+name).encode('utf8')))) for name, proptype in props }
def _get_list(self, prefix, props):
count = int(_ensure_encoding(_mpv_get_property_string(self.handle, (prefix+'count').encode('utf8'))))
return [ self._get_dict(prefix+str(index)+'/', props) for index in range(count)]
# TODO: af, vf properties
# TODO: edition-list
# TODO property-mapped options
def bindproperty(MPV, name, proptype, access):
def getter(self):
return proptype(_ensure_encoding(_mpv_get_property_string(self.handle, name.encode('utf8'))))
def setter(self, value):
_mpv_set_property_string(self.handle, name.encode('utf8'), str(proptype(value)).encode('utf8'))
def barf(*args):
raise NotImplementedError('Access denied')
setattr(MPV, name.replace('-', '_'), property(getter if 'r' in access else barf, setter if 'w' in access else barf))
for name, proptype, access in (
('osd-level', int, 'rw'),
('osd-scale', float, 'rw'),
('loop', str, 'rw'),
('loop-file', str, 'rw'),
('speed', float, 'rw'),
('filename', str, 'r'),
('file-size', int, 'r'),
('path', str, 'r'),
('media-title', str, 'r'),
('stream-pos', int, 'rw'),
('stream-end', int, 'r'),
('length', float, 'r'),
('avsync', float, 'r'),
('total-avsync-change', float, 'r'),
('drop-frame-count', int, 'r'),
('percent-pos', int, 'rw'),
('ratio-pos', float, 'rw'),
('time-pos', float, 'rw'),
('time-start', float, 'r'),
('time-remaining', float, 'r'),
('playtime-remaining', float, 'r'),
('chapter', int, 'rw'),
('edition', int, 'rw'),
('disc-titles', int, 'r'),
('disc-title', str, 'rw'),
('disc-menu-active', ynbool, 'r'),
('chapters', int, 'r'),
('editions', int, 'r'),
('angle', int, 'rw'),
('pause', ynbool, 'rw'),
('core-idle', ynbool, 'r'),
('cache', int, 'r'),
('cache-size' , int, 'rw'),
('pause-for-cache', ynbool, 'r'),
('eof-reached', ynbool, 'r'),
('pts-association-mode', str, 'rw'),
('hr-seek', ynbool, 'rw'),
('volume', float, 'rw'),
('mute', ynbool, 'rw'),
('audio-delay', float, 'rw'),
('audio-format', str, 'r'),
('audio-codec', str, 'r'),
('audio-bitrate', float, 'r'),
('audio-samplerate', int, 'r'),
('audio-channels', str, 'r'),
('aid', int, 'rw'),
('audio', int, 'rw'),
('balance', int, 'rw'),
('fullscreen', ynbool, 'rw'),
('deinterlace', str, 'rw'),
('colormatrix', str, 'rw'),
('colormatrix-input-range', str, 'rw'),
('colormatrix-output-range', str, 'rw'),
('colormatrix-primaries', str, 'rw'),
('ontop', ynbool, 'rw'),
('border', ynbool, 'rw'),
('framedrop', str, 'rw'),
('gamma', float, 'rw'),
('brightness', int, 'rw'),
('contrast', int, 'rw'),
('saturation', int, 'rw'),
('hue', int, 'rw'),
('hwdec', ynbool, 'rw'),
('panscan', float, 'rw'),
('video-format', str, 'r'),
('video-codec', str, 'r'),
('video-bitrate', float, 'r'),
('width', int, 'r'),
('height', int, 'r'),
('dwidth', int, 'r'),
('dheight', int, 'r'),
('fps', float, 'r'),
('estimated-vf-fps', float, 'r'),
('window-scale', float, 'rw'),
('video-aspect', str, 'rw'),
('osd-width', int, 'r'),
('osd-height', int, 'r'),
('osd-par', float, 'r'),
('vid', int, 'rw'),
('video', int, 'rw'),
('video-align-x', float, 'rw'),
('video-align-y', float, 'rw'),
('video-pan-x', int, 'rw'),
('video-pan-y', int, 'rw'),
('video-zoom', float, 'rw'),
('video-unscaled', ynbool, 'w'),
('program', int, 'w'),
('sid', int, 'rw'),
('secondary-sid', int, 'rw'),
('sub', int, 'rw'),
('sub-delay', float, 'rw'),
('sub-pos', int, 'rw'),
('sub-visibility', ynbool, 'rw'),
('sub-forced-only', ynbool, 'rw'),
('sub-scale', float, 'rw'),
('ass-use-margins', ynbool, 'rw'),
('ass-vsfilter-aspect-compat', ynbool, 'rw'),
('ass-style-override', str, 'rw'),
('stream-capture', str, 'rw'),
('tv-brightness', int, 'rw'),
('tv-contrast', int, 'rw'),
('tv-saturation', int, 'rw'),
('tv-hue', int, 'rw'),
('playlist-pos', int, 'rw'),
('playlist-count', int, 'r'),
('quvi-format', str, 'rw'),
('seekable', ynbool, 'r')):
bindproperty(MPV, name, proptype, access)

24
omp/__init__.py Normal file
View File

@ -0,0 +1,24 @@
# omp - Omni Media Player
# This is a part of comp
#
# comp 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.
#
# comp 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 comp. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright (C) 2017 Nguyễn Gia Phong <vn.mcsinyx@gmail.com>
"""Omni Media Player - an handy mpv front-end library for interactive
control.
"""
from .ie import extract_info
from .omp import Omp

125
omp/ie.py Normal file
View File

@ -0,0 +1,125 @@
# ie.py - Omni Media Player infomation extractor
# This file is part of comp
#
# comp 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.
#
# comp 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 comp. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright (C) 2017 Nguyễn Gia Phong <vn.mcsinyx@gmail.com>
import json
from os.path import abspath, expanduser, expandvars, isfile
from time import gmtime, sleep, strftime
from urllib.request import urlretrieve
from youtube_dl import YoutubeDL
from mpv import MPV
DEFAULT_ENTRY = {'filename': '', 'title': '', 'duration': '00:00:00',
'error': False, 'playing': False, 'selected': False}
JSON_KEYS = 'filename', 'title', 'duration', 'error', 'selected'
class YoutubeDLLogger:
def debug(self, msg): pass
def warning(self, msg): pass
def error(self, msg): pass
def json_extract_info(filename):
"""Return list of entries extracted from a file using json. If an
error occur during the extraction, return None.
"""
try:
if not isfile(filename):
filename = urlretrieve(filename)[0]
with open(filename) as f: raw_info, info = json.load(f), []
for i in raw_info:
e = DEFAULT_ENTRY.copy()
for k in e:
if k in i and isinstance(i[k], type(e[k])): e[k] = i[k]
info.append(e)
except:
return None
else:
return info
def mpv_extract_info(filename):
"""Return list of entries extracted from a path or URL using mpv. If
an error occur during the extraction, return None.
"""
mp = MPV(ytdl=True, vid=False)
mp.play(filename)
while mp.duration is None:
sleep(0.25)
if mp.playback_abort: return None
info = {'filename': filename, 'title': mp.media_title.decode(),
'duration': mp.osd.duration, 'error': False, 'playing': False,
'selected': False}
mp.quit()
return [info]
def ytdl_extract_info(filename):
"""Return list of entries extracted from a path or URL using
youtube-dl. If an error occur during the extraction, return None.
"""
ytdl_opts = {'logger': YoutubeDLLogger(), 'default_search': 'ytsearch',
'extract_flat': 'in_playlist'}
with YoutubeDL(ytdl_opts) as ytdl:
try:
raw_info = ytdl.extract_info(filename, download=False)
except:
return None
info = raw_info.get('entries', [raw_info])
for i in info:
if 'webpage_url' in i:
i['filename'] = i['webpage_url']
elif (i.get('ie_key') == 'Youtube'
or i.get('extractor') == 'youtube'):
i['filename'] = 'https://youtu.be/' + i['id']
else:
i['filename'] = i['url']
if 'title' not in i:
try:
i['title'] = ytdl.extract_info(i['filename'],
download=False)['title']
except:
return None
if 'duration' not in i:
i['duration'] = '00:00:00'
elif isinstance(i['duration'], int):
i['duration'] = strftime('%H:%M:%S', gmtime(i['duration']))
for k in 'error', 'playing', 'selected': i.setdefault(k, False)
for k in i.copy():
if k not in DEFAULT_ENTRY: i.pop(k)
return info
def extract_info(filename, extractor='youtube-dl'):
"""Return list of entries extracted from a path or URL using
specified extractor. If an error occur during the extraction,
return None.
The extractor could be either 'json', 'mpv' or 'youtube-dl' and
fallback to 'youtube-dl'.
"""
if isfile(expanduser(expandvars(filename))):
filename = abspath(expanduser(expandvars(filename)))
if extractor == 'json':
return json_extract_info(filename)
elif extractor == 'mpv':
return mpv_extract_info(filename)
else:
return ytdl_extract_info(filename)

Binary file not shown.

View File

@ -0,0 +1,72 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: 2017-04-05 11:00+0700\n"
"PO-Revision-Date: 2017-04-06 22:29+0700\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: pygettext.py 1.5\n"
"X-Generator: Poedit 1.8.11\n"
"Last-Translator: \n"
"Plural-Forms: nplurals=1; plural=0;\n"
"Language: vi_VN\n"
msgid "Curses Online Media Player"
msgstr "Phần mềm chơi đa phương tiện trực tuyến sử dụng curses"
msgid "play-current"
msgstr "chơi-một"
msgid "play-all"
msgstr "chơi-tất-cả"
msgid "play-selected"
msgstr "chơi-đã-chọn"
msgid "repeat-current"
msgstr "lặp-một"
msgid "repeat-all"
msgstr "lặp-tất-cả"
msgid "repeat-selected"
msgstr "lặp-đã-chọn"
msgid "shuffle-all"
msgstr "ngẫu-nhiên-tất-cả"
msgid "shuffle-selected"
msgstr "ngẫu-nhiên-đã-chọn"
msgid "URL"
msgstr "URL"
msgid "Title"
msgstr "Tiêu đề"
msgid "Source"
msgstr "Nguồn"
msgid "Current size: {}x{}. Minimum size: {}x4."
msgstr "Kích thước hiện tại: {}x{}. Kích thước tối thiểu: {}x4."
msgid "Save playlist to [{}]:"
msgstr "Lưu playlist tại [{}]:"
msgid "'{}': Can't open file for writing"
msgstr "'{}': Không mở được tệp để ghi"
msgid "'{}' written"
msgstr "'{}' đã ghi"
msgid "path to playlist in JSON format"
msgstr "đường dẫn đến playlist ở định dạng JSON"
msgid "URL to an playlist on Youtube"
msgstr "URL của playlist trên Youtube"

221
omp/omp.py Normal file
View File

@ -0,0 +1,221 @@
# omp.py - Omni Media Player meta object
# This file is part of comp
#
# comp 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.
#
# comp 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 comp. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright (C) 2017 Nguyễn Gia Phong <vn.mcsinyx@gmail.com>
import curses
import json
import re
from bisect import bisect_left as bisect
from collections import deque
from gettext import bindtextdomain, gettext as _, textdomain
from itertools import cycle
from os import makedirs
from os.path import abspath, dirname, expanduser, expandvars
from random import choice
from sys import exc_info
from pkg_resources import resource_filename
from mpv import MPV
from .ie import JSON_KEYS
# Init gettext
bindtextdomain('omp', resource_filename('omp', 'locale'))
textdomain('omp')
def shuffle_init(a):
"""Return in iterator which yield random elements from a,
and always begin with its first element.
"""
if a:
yield a[0]
while True: yield choice(a)
class Omp(object):
"""Omni Media Player meta object.
Attributes:
entries (list): list of all tracks
json_file (str): path to save JSON playlist
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
search_res (iterator): title-searched results
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, json_file, mode, mpv_args, ytdlf):
self = object.__new__(cls)
self.play_backward, self.reading = False, False
self.playing = -1
self.json_file, self.mode = json_file, mode
self.entries, self.played = entries, []
self.playlist, self.search_res = iter(()), deque()
self.mp = MPV(input_default_bindings=True, input_vo_keyboard=True,
ytdl=True, ytdl_format=ytdlf)
return self
def __init__(self, entries, json_file, mode, mpv_args, ytdlf):
for arg, val in mpv_args.items():
try:
self.mp[arg] = val
except:
self.__exit__(*exc_info())
@self.mp.property_observer('mute')
@self.mp.property_observer('pause')
@self.mp.property_observer('time-pos')
def observer(name, value): self.property_handler(name, value)
self.mp.register_key_binding('q', lambda state, key: None)
def __enter__(self): return self
def seek(self, amount, reference='relative', precision='default-precise'):
"""Wrap a try clause around mp.seek to avoid crashing when
nothing is being played.
"""
try:
self.mp.seek(amount, reference, precision)
except:
self.print_msg(_("Failed to seek"), error=True)
def add(self, name, value=1):
"""Wrap a try clause around mp.property_add."""
try:
self.mp.property_add(name, value)
except:
self.print_msg(
_("Failed to add {} to '{}'").format(value, name), error=True)
def multiply(self, name, factor):
"""Wrap a try clause around mp.property_multiply."""
try:
self.mp.property_multiply(name, factor)
except:
self.print_msg(
_("Failed to multiply '{}' with {}").format(name, factor),
error=True)
def cycle(self, name, direction='up'):
"""Wrap a try clause around mp.cycle."""
try:
self.mp.cycle(name, direction='up')
except:
self.print_msg(
_("Failed to cycle {} '{}'").format(direction, name),
error=True)
def idx(self, entry=None):
"""Return the index of the current entry."""
if entry is None:
return self.start + self.y - 1
return self.entries.index(entry)
def current(self):
"""Return the current entry."""
try:
return self.entries[self.idx()]
except:
return {}
def update_playlist(self):
"""Update the playlist to be used by play function."""
action, pick = self.mode.split('-')
if pick == 'current':
self.play_list = deque([self.current()])
elif pick == 'all':
self.play_list = deque(self.entries)
self.play_list.rotate(-self.idx())
elif pick == 'selected':
self.play_list = deque([entry for entry in self.entries
if entry.get('selected')])
indexes = [i for i, entry in enumerate(self.entries)
if entry.get('selected')]
idx = indexes[bisect(indexes, self.idx())]
self.play_list.rotate(-self.play_list.index(self.entries[idx]))
if action == 'play':
self.playlist = iter(self.play_list)
elif action == 'repeat':
self.playlist = cycle(self.play_list)
elif action == 'shuffle':
self.playlist = shuffle_init(self.play_list)
if self.playing < -1: self.played = self.played[:self.playing+1]
def next(self, force=False, backward=False):
"""Go forward/backward in the playlist.
If forced, this will also unpause the player.
"""
self.play_backward = backward
if self.mp.idle_active:
self.play(force)
else:
self.mp.time_pos = self.mp.duration
if force: self.mp.pause = False
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))
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 dump_json(self):
"""Read user input needed to save the playlist."""
s = self.read_input(
_("Save playlist to [{}]: ").format(self.json_file))
self.json_file = abspath(expanduser(expandvars(s or self.json_file)))
entries = [{k: v for k, v in entry.items() if k in JSON_KEYS}
for entry in self.entries]
try:
makedirs(dirname(self.json_file), exist_ok=True)
with open(self.json_file, 'w') as f:
json.dump(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(self.json_file))
def __exit__(self, exc_type, exc_value, traceback):
self.mp.quit()

21
settings.ini Normal file
View File

@ -0,0 +1,21 @@
[comp]
# Initial playing mode, which can be one of these 8 modes: play-current,
# play-all, play-selected, repeat-current, repeat-all, repeat-selected,
# shuffle-all and shuffle-selected.
play-mode = play-current
[mpv]
# Options to be parsed to mpv. See OPTIONS section on mpv(1) man pages for
# its complete list of available options.
# For example:
#vo = xv
#ontop = yes
#border = no
#force-window = yes
#autofit = 500x280
#geometry = -15-50
[youtube-dl]
# Video format/quality to be passed to youtube-dl. See FORMAT SELECTION in
# youtube-dl(1) man page for more details and descriptions.
format = bestvideo+bestaudio

View File

@ -1,13 +1,36 @@
#!/usr/bin/env python3
from setuptools import setup, find_packages
from setuptools import setup
with open('README.rst') as f:
long_description = f.read()
setup(
name = 'mpv',
version = '0.1',
py_modules = ['mpv'],
description = ('A python interface to the mpv media player'),
url = 'https://github.com/jaseg/python-mpv',
author = 'jaseg',
author_email = 'github@jaseg.net',
license = 'AGPLv2'
)
name='comp',
version='0.4.6',
description=('Curses Omni Media Player'),
long_description=long_description,
url='https://github.com/McSinyx/comp',
author='Nguyễn Gia Phong',
author_email='vn.mcsinyx@gmail.com',
license='AGPLv3+',
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Console :: Curses',
'Intended Audience :: End Users/Desktop',
'License :: OSI Approved :: GNU Affero General Public License v3',
'Natural Language :: English',
'Natural Language :: Vietnamese',
'Operating System :: POSIX',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Multimedia :: Sound/Audio :: Players',
'Topic :: Multimedia :: Video :: Display'],
keywords='youtube-dl mpv-wrapper curses console-application multimedia',
packages=['omp'],
install_requires=['python-mpv', 'youtube-dl'],
python_requires='>=3.5',
package_data={'omp': ['locale/*/LC_MESSAGES/omp.mo']},
data_files=[('share/man/man1', ['doc/comp.1'])],
scripts=['comp'],
platforms=['POSIX'])

BIN
test/gplv3.ogg Normal file

Binary file not shown.

198
test/playlist.json Normal file
View File

@ -0,0 +1,198 @@
[
{
"duration": "00:05:21",
"error": false,
"filename": "https://youtu.be/weeI1G46q0o",
"selected": true,
"title": "DJ Khaled - I'm the One ft. Justin Bieber, Quavo, Chance the Rapper, Lil Wayne"
},
{
"duration": "00:04:23",
"error": false,
"filename": "https://youtu.be/JGwWNGJdvx8",
"selected": true,
"title": "Ed Sheeran - Shape of You [Official Video]"
},
{
"duration": "00:03:48",
"error": false,
"filename": "https://youtu.be/72UO0v5ESUo",
"selected": true,
"title": "Luis Fonsi, Daddy Yankee - Despacito (Audio) ft. Justin Bieber"
},
{
"duration": "00:03:41",
"error": false,
"filename": "https://youtu.be/D5drYkLiLI8",
"selected": false,
"title": "Kygo, Selena Gomez - It Ain't Me (with Selena Gomez) (Audio)"
},
{
"duration": "00:12:53",
"error": false,
"filename": "test/gplv3.ogg",
"selected": true,
"title": "gplv3.ogg"
},
{
"duration": "00:04:17",
"error": false,
"filename": "https://youtu.be/dPI-mRFEIH0",
"selected": false,
"title": "Katy Perry - Bon Appétit (Official) ft. Migos"
},
{
"duration": "00:04:06",
"error": false,
"filename": "https://youtu.be/aatr_2MstrI",
"selected": false,
"title": "Clean Bandit - Symphony feat. Zara Larsson [Official Video]"
},
{
"duration": "00:04:16",
"error": false,
"filename": "https://youtu.be/7F37r50VUTQ",
"selected": true,
"title": "ZAYN, Taylor Swift - I Dont Wanna Live Forever (Fifty Shades Darker)"
},
{
"duration": "00:04:57",
"error": false,
"filename": "https://youtu.be/qFLhGq0060w",
"selected": true,
"title": "The Weeknd - I Feel It Coming ft. Daft Punk"
},
{
"duration": "00:02:29",
"error": false,
"filename": "http://www.html5videoplayer.net/videos/toystory.mp4",
"selected": false,
"title": "toystory.mp4"
},
{
"duration": "00:05:13",
"error": false,
"filename": "https://youtu.be/6ImFf__U6io",
"selected": true,
"title": "Birdman - Dark Shades (Explicit) ft. Lil Wayne, Mack Maine"
},
{
"duration": "00:03:55",
"error": false,
"filename": "https://www.youtube.com/watch?v=3M3xfu0m5o4",
"selected": false,
"title": "David Banner - Play (Dirty version)"
},
{
"duration": "00:04:14",
"error": false,
"filename": "https://www.youtube.com/watch?v=NmCFY1oYDeM",
"selected": false,
"title": "John Legend - Love Me Now (Video)"
},
{
"duration": "00:03:55",
"error": false,
"filename": "https://youtu.be/NGLxoKOvzu4",
"selected": false,
"title": "Jason Derulo - Swalla (feat. Nicki Minaj & Ty Dolla $ign) (Official Music Video)"
},
{
"duration": "00:03:51",
"error": false,
"filename": "https://youtu.be/nfs8NYg7yQM",
"selected": false,
"title": "Charlie Puth - Attention [Official Video]"
},
{
"duration": "00:04:10",
"error": false,
"filename": "https://www.youtube.com/watch?v=sRIkXM8S1J8",
"selected": true,
"title": "Best Goat Song Versions Compilation Ever! (HD)"
},
{
"duration": "00:04:03",
"error": false,
"filename": "https://youtu.be/dMK_npDG12Q",
"selected": false,
"title": "Lorde - Green Light"
},
{
"duration": "00:03:32",
"error": false,
"filename": "https://youtu.be/h--P8HzYZ74",
"selected": true,
"title": "Zedd, Alessia Cara - Stay (Lyric Video)"
},
{
"duration": "00:02:45",
"error": false,
"filename": "https://youtu.be/Mdh2p03cRfw",
"selected": false,
"title": "Sam Hunt - Body Like A Back Road (Audio)"
},
{
"duration": "00:03:40",
"error": false,
"filename": "https://youtu.be/Fq0xEpRDL9Q",
"selected": false,
"title": "Chris Brown - Privacy (Explicit Version)"
},
{
"duration": "00:03:36",
"error": false,
"filename": "https://youtu.be/7wtfhZwyrcc",
"selected": false,
"title": "Imagine Dragons - Believer"
},
{
"duration": "00:03:52",
"error": false,
"filename": "https://youtu.be/A-Rn0iQEpc8",
"selected": false,
"title": "Can't Stop the SUSE - (Can't Stop the Feeling parody)"
},
{
"duration": "00:04:28",
"error": false,
"filename": "https://youtu.be/SYRlTISvjww",
"selected": false,
"title": "Uptime Funk - (Uptown Funk parody)"
},
{
"duration": "00:00:00",
"error": false,
"filename": "https://youtu.be/VNkDJk5_9eU",
"selected": false,
"title": "What Does the Chameleon Say? (Ylvis - What Does the Fox Say parody)"
},
{
"duration": "00:03:46",
"error": false,
"filename": "https://youtu.be/M9bq_alk-sw",
"selected": false,
"title": "SUSE. Yes Please. (Maroon 5 - Sugar parody)"
},
{
"duration": "00:03:30",
"error": false,
"filename": "https://youtu.be/oHNKTlz1lps",
"selected": true,
"title": "Linus Said - Music Parody (Momma Said)"
},
{
"duration": "00:03:58",
"error": false,
"filename": "https://youtu.be/4VrhlyIgo3M",
"selected": false,
"title": "25 Years - SUSE Music Video (7 Years parody)"
},
{
"duration": "00:03:52",
"error": false,
"filename": "https://youtu.be/9sg-A-eS6Ig",
"selected": true,
"title": "Enrique Iglesias - SUBEME LA RADIO (Official Video) ft. Descemer Bueno, Zion & Lennox"
}
]