Compare commits
47 Commits
Author | SHA1 | Date |
---|---|---|
Nguyễn Gia Phong | e860f1526d | |
Nguyễn Gia Phong | 6576c01801 | |
cclauss | 900a3f359a | |
Nguyễn Gia Phong | 7203eaaef5 | |
cclauss | 51ba038b80 | |
Nguyễn Gia Phong | be0eaeadb7 | |
Nguyễn Gia Phong | f8185d1b31 | |
Nguyễn Gia Phong | 082685cee3 | |
Nguyễn Gia Phong | 0500d98b0a | |
Nguyễn Gia Phong | bb29631789 | |
Nguyễn Gia Phong | f4791e6e99 | |
Nguyễn Gia Phong | 37f92bcc74 | |
Nguyễn Gia Phong | e21fd6285d | |
Nguyễn Gia Phong | 4b18b5c1bf | |
Nguyễn Gia Phong | 6ada63f856 | |
Nguyễn Gia Phong | b01b6abd1a | |
Nguyễn Gia Phong | 7542fb3892 | |
Nguyễn Gia Phong | 1b0692417f | |
Nguyễn Gia Phong | ba3a065006 | |
Nguyễn Gia Phong | 04901f33d9 | |
Nguyễn Gia Phong | 9cef1e2382 | |
Nguyễn Gia Phong | 2972111b59 | |
Nguyễn Gia Phong | 6d0aa7fe51 | |
Nguyễn Gia Phong | 801c439146 | |
Nguyễn Gia Phong | 031f9ea1aa | |
Nguyễn Gia Phong | 654c5572ef | |
Nguyễn Gia Phong | 9129a8974f | |
Nguyễn Gia Phong | 98c73ae8ac | |
Nguyễn Gia Phong | 0bca71fe0c | |
Nguyễn Gia Phong | 8334263141 | |
Nguyễn Gia Phong | 056727768d | |
Nguyễn Gia Phong | d146f5d74c | |
Nguyễn Gia Phong | df98e90366 | |
Nguyễn Gia Phong | cf9cfd3c01 | |
Nguyễn Gia Phong | fc6b4a0c51 | |
Nguyễn Gia Phong | c1b0652078 | |
Nguyễn Gia Phong | 6a2c20e490 | |
Nguyễn Gia Phong | 3679965578 | |
Nguyễn Gia Phong | 23fc9ae49b | |
Nguyễn Gia Phong | 5abe0b2ebc | |
Nguyễn Gia Phong | 3f603ed693 | |
Nguyễn Gia Phong | d5117c65c8 | |
Nguyễn Gia Phong | 1b130724d2 | |
Nguyễn Gia Phong | c9fcb30f75 | |
Nguyễn Gia Phong | 1e1418f8dd | |
Nguyễn Gia Phong | 7066eeb697 | |
jaseg | f8b6ac8f66 |
|
@ -1,3 +1,5 @@
|
|||
MANIFEST
|
||||
build
|
||||
comp.egg-info
|
||||
dist
|
||||
mpv.egg-info
|
||||
__pycache__
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
include LICENSE README.rst doc/screenshot.png
|
|
@ -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.
|
|
@ -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.
|
|
@ -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)
|
|
@ -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)
|
Binary file not shown.
After Width: | Height: | Size: 422 KiB |
Binary file not shown.
|
@ -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
594
mpv.py
|
@ -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)
|
||||
|
|
@ -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
|
|
@ -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.
|
@ -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"
|
|
@ -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()
|
|
@ -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
|
43
setup.py
43
setup.py
|
@ -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'])
|
||||
|
|
Binary file not shown.
|
@ -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 Don’t 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"
|
||||
}
|
||||
]
|
Loading…
Reference in New Issue