Add save playlist function and complete Vietnamese translation

This commit is contained in:
Nguyễn Gia Phong 2017-04-06 23:02:54 +07:00
parent c224c24b84
commit 894fa77fd6
7 changed files with 162 additions and 105 deletions

View File

@ -47,37 +47,41 @@ Usage
Keyboard control
^^^^^^^^^^^^^^^^
+--------------+-------------------------------+
| Key | Action |
+==============+===============================+
| ``h``, Up | Move a single line up |
+--------------+-------------------------------+
| ``j``, Down | Move a single line down |
+--------------+-------------------------------+
| Page Up | Move a single page up |
+--------------+-------------------------------+
| Page Down | Move a single page down |
+--------------+-------------------------------+
| Home | Move to the begin of the list |
+--------------+-------------------------------+
| End | Move to the end of the list |
+--------------+-------------------------------+
| Left | Seek backward 5 seconds |
+--------------+-------------------------------+
| Right | Seek forward 5 seconds |
+--------------+-------------------------------+
| ``c`` | Select the current track |
+--------------+-------------------------------+
| ``p`` | Start playing |
+--------------+-------------------------------+
| Space | Toggle pause |
+--------------+-------------------------------+
| ``m``, ``M`` | Cycle through playing modes |
+--------------+-------------------------------+
| ``A`` | Toggle mute |
+--------------+-------------------------------+
| ``V`` | Toggle video |
+--------------+-------------------------------+
+--------------+---------------------------------------------+
| Key | Action |
+==============+=============================================+
| ``h``, Up | Move a single line up |
+--------------+---------------------------------------------+
| ``j``, Down | Move a single line down |
+--------------+---------------------------------------------+
| Page Up | Move a single page up |
+--------------+---------------------------------------------+
| Page Down | Move a single page down |
+--------------+---------------------------------------------+
| Home | Move to the begin of the list |
+--------------+---------------------------------------------+
| End | Move to the end of the list |
+--------------+---------------------------------------------+
| Left | Seek backward 5 seconds |
+--------------+---------------------------------------------+
| Right | Seek forward 5 seconds |
+--------------+---------------------------------------------+
| ``c`` | Select the current track |
+--------------+---------------------------------------------+
| ``p`` | Start playing |
+--------------+---------------------------------------------+
| Space | Toggle pause |
+--------------+---------------------------------------------+
| ``m``, ``M`` | Cycle through playing modes |
+--------------+---------------------------------------------+
| ``A`` | Toggle mute |
+--------------+---------------------------------------------+
| ``V`` | Toggle video |
+--------------+---------------------------------------------+
| ``S`` | Save the current playlist under JSON format |
+--------------+---------------------------------------------+
| F5 | Reprint the screen content |
+--------------+---------------------------------------------+
Configuration files
-------------------

126
comp
View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3
# comp - Curses Online Media Player
# Copyright (C) 2017 Raphael McSinyx
# Copyright (C) 2017 Nguyễn Gia Phong
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@ -21,12 +21,13 @@ import json
import subprocess
from argparse import ArgumentParser
from configparser import ConfigParser
from curses.ascii import ctrl
from datetime import datetime
from gettext import bindtextdomain, gettext, textdomain
from io import StringIO
from itertools import cycle
from os import linesep, makedirs
from os.path import dirname, expanduser, isfile
from os.path import abspath, dirname, expanduser, isfile
from random import choice
from time import gmtime, strftime
from threading import Thread
@ -34,25 +35,16 @@ from threading import Thread
from mpv import MPV
# Init gettext
bindtextdomain('comp', 'locale')
#bindtextdomain('comp', 'locale')
textdomain('comp')
_ = gettext
GLOBAL_CONFIG = '/etc/comp/settings.ini'
SYSTEM_CONFIG = '/etc/comp/settings.ini'
USER_CONFIG = expanduser('~/.config/comp/settings.ini')
MPV_LOG = expanduser('~/.cache/comp/mpv.log')
MODES = ('play-current', 'play-all', 'play-selected', 'repeat-current',
'repeat-all', 'repeat-selected', 'shuffle-all', 'shuffle-selected')
# I ain't found the correct way to do this yet
_MODES = {'play-current': _('play-current'),
'play-all': _('play-all'),
'play-selected': _('play-selected'),
'repeat-current': _('repeat-current'),
'repeat-all': _('repeat-all'),
'repeat-selected': _('repeat-selected'),
'shuffle-all': _('shuffle-all'),
'shuffle-selected': _('shuffle-selected')}
MODE_STR_LEN = max(len(mode) for mode in _MODES.values())
MODE_STR_LEN = max(len(_(mode)) for mode in MODES)
def mpv_logger(loglevel, component, message):
@ -63,37 +55,45 @@ def mpv_logger(loglevel, component, message):
def setno(entries, keys):
"""Set all keys of each track in entries to False."""
"""Set all keys of each entry in entries to False."""
for key in keys:
for track in entries:
track[key] = False
for entry in entries:
entry[key] = False
def getlink(entry):
links = {'Youtube': 'https://youtu.be/{}'}
# This is not fail-safe
entry['url'] = links.get(entry.get('ie_key', ''), '{}').format(entry.get('id'))
def playlist(mode):
"""Return a generator of tracks to be played."""
"""Return a generator of entries to be played."""
action, choose_from = mode.split('-')
if choose_from == 'all':
tracks = entries
entries2play = entries
else:
tracks = [track for track in entries
if track.setdefault(choose_from, False)]
entries2play = [entry for entry in entries
if entry.setdefault(choose_from, False)]
# Somehow yield have to be used instead of returning a generator
if action == 'play':
for track in tracks: yield track
for entry in entries2play: yield entry
elif action == 'repeat':
for track in cycle(tracks): yield track
elif tracks:
while True: yield choice(tracks)
for entry in cycle(entries2play): yield entry
elif entries2play:
while True: yield choice(entries2play)
def play():
for track in playlist(mode):
entries[entries.index(track)]['playing'] = True
for entry in playlist(mode):
idx = entries.index(entry)
entries[idx]['playing'] = True
reprint(stdscr, entries[start : start+curses.LINES-3])
# Gross hack
mp.play('https://youtu.be/' + track['url'])
getlink(entries[idx])
mp.play(entries[idx]['url'])
mp.wait_for_playback()
entries[entries.index(track)]['playing'] = False
entries[idx]['playing'] = False
reprint(stdscr, entries[start : start+curses.LINES-3])
@ -112,7 +112,7 @@ def secpair2hhmmss(pos, duration):
def update_status_line(stdscr, mp):
left = ' ' + secpair2hhmmss(mp._get_property('time-pos', int),
mp._get_property('duration', int))
right = ' {} {}{} '.format(_MODES[mode],
right = ' {} {}{} '.format(_(mode),
' ' if mp._get_property('mute', bool) else 'A',
' ' if mp._get_property('vid') == 'no' else 'V')
if left != ' ':
@ -127,16 +127,18 @@ def update_status_line(stdscr, mp):
else:
stdscr.addstr(curses.LINES - 2, 0, right.rjust(curses.COLS),
curses.color_pair(8))
stdscr.move(curses.LINES - 1, 0)
stdscr.clrtoeol()
stdscr.refresh()
def reattr(stdscr, y, track):
invert = 8 if track.setdefault('current', False) else 0
if track.setdefault('error', False):
def reattr(stdscr, y, entry):
invert = 8 if entry.setdefault('current', False) else 0
if entry.setdefault('error', False):
stdscr.chgat(y, 0, curses.color_pair(1 + invert) | curses.A_BOLD)
elif track.setdefault('playing', False):
elif entry.setdefault('playing', False):
stdscr.chgat(y, 0, curses.color_pair(3 + invert) | curses.A_BOLD)
elif track.setdefault('selected', False):
elif entry.setdefault('selected', False):
stdscr.chgat(y, 0, curses.color_pair(5 + invert) | curses.A_BOLD)
elif invert:
stdscr.chgat(y, 0, curses.color_pair(12) | curses.A_BOLD)
@ -144,16 +146,17 @@ def reattr(stdscr, y, track):
stdscr.chgat(y, 0, curses.color_pair(0) | curses.A_NORMAL)
def reprint(stdscr, tracks2print):
def reprint(stdscr, entries2print):
stdscr.clear()
stdscr.addstr(0, curses.COLS-12, _('URL'))
stdscr.addstr(0, 1, _('Title'))
sitenamelen = max(max(len(entry['ie_key']) for entry in entries), 6)
stdscr.addstr(0, curses.COLS - sitenamelen - 1, _('Source'))
stdscr.chgat(0, 0, curses.color_pair(10) | curses.A_BOLD)
for i, track in enumerate(tracks2print):
for i, entry in enumerate(entries2print):
y = i + 1
stdscr.addstr(y, 0, track['url'].rjust(curses.COLS - 1))
stdscr.addstr(y, 1, track['title'][:curses.COLS-14])
reattr(stdscr, y, track)
stdscr.addstr(y, 0, entry['ie_key'].rjust(curses.COLS - 1))
stdscr.addstr(y, 1, entry['title'][:curses.COLS-sitenamelen-3])
reattr(stdscr, y, entry)
update_status_line(stdscr, mp)
@ -199,7 +202,7 @@ def move(stdscr, entries, y, delta):
return y
parser = ArgumentParser(description="Curses Online Media Player")
parser = ArgumentParser(description=_("Curses Online Media Player"))
parser.add_argument('-j', '--json-playlist', required=False,
help=_('path to playlist in JSON format'))
parser.add_argument('-y', '--youtube-playlist', required=False,
@ -207,7 +210,7 @@ parser.add_argument('-y', '--youtube-playlist', required=False,
args = parser.parse_args()
config = ConfigParser()
config.read(USER_CONFIG if isfile(USER_CONFIG) else GLOBAL_CONFIG)
config.read(USER_CONFIG if isfile(USER_CONFIG) else SYSTEM_CONFIG)
mode = config.get('comp', 'play-mode', fallback='play-current')
video = config.get('mpv', 'video', fallback='auto')
video_output = config.get('mpv', 'video-output', fallback=None)
@ -215,7 +218,7 @@ ytdlf = config.get('youtube-dl', 'format', fallback='best')
if args.json_playlist:
with open(args.json_playlist) as f:
entries = json.load(f)['entries']
entries = json.load(f)
elif args.youtube_playlist:
# Extremely gross hack
raw_json = subprocess.run(['youtube-dl', '--flat-playlist',
@ -264,22 +267,26 @@ y = 1
entries[0]['current'] = True
reprint(stdscr, entries[:curses.LINES-3])
file = '' # initial path of the file to dump the current playlist
c = stdscr.getch()
while c != 113: # letter q
if c == curses.KEY_RESIZE:
curses.update_lines_cols()
if curses.COLS < MODE_STR_LEN + 42 or curses.LINES < 4:
stdscr.clear()
scr_size_warn = 'Current size: {}x{}. Minimum size: {}x4.'.format(
sizeerr = _('Current size: {}x{}. Minimum size: {}x4.').format(
curses.COLS,
curses.LINES,
MODE_STR_LEN + 42
)
stdscr.addstr(0, 0, scr_size_warn)
stdscr.addstr(0, 0, sizeerr)
else:
start += y - 1
y = 1
reprint(stdscr, entries[start : start+curses.LINES-3])
elif c == curses.KEY_F5: # F5
reprint(stdscr, entries[start : start+curses.LINES-3])
elif c in (107, curses.KEY_UP): # letter k or up arrow
y = move(stdscr, entries, y, -1)
elif c in (106, curses.KEY_DOWN): # letter j or down arrow
@ -299,7 +306,8 @@ while c != 113: # letter q
if mp._get_property('duration', int):
mp.seek(2.5)
elif c == 99: # letter c
entries[start + y - 1]['selected'] = not entries[start + y - 1]['selected']
i = start + y - 1
entries[i]['selected'] = not entries[i].setdefault('selected', False)
y = move(stdscr, entries, y, 1)
elif c == 112: # letter p
mp._set_property('pause', False, bool)
@ -307,7 +315,7 @@ while c != 113: # letter q
play_thread.daemon = True
play_thread.start()
elif c == 32: # space
mp._set_property('pause', not mp._get_property('pause', bool), bool)
mp._toggle_property('pause')
elif c == 109: # letter m
mode = MODES[(MODES.index(mode) + 1) % 8]
update_status_line(stdscr, mp)
@ -315,10 +323,30 @@ while c != 113: # letter q
mode = MODES[(MODES.index(mode) - 1) % 8]
update_status_line(stdscr, mp)
elif c == 65: # letter A
mp._set_property('mute', not mp._get_property('mute', bool), bool)
mp._toggle_property('mute')
elif c == 86: # letter V
mp._set_property('vid',
'auto' if mp._get_property('vid') == 'no' else 'no')
elif c == 83: # letter S
prompt = _('Save playlist to [{}]:').format(file)
stdscr.addstr(curses.LINES - 1, 0, prompt)
curses.curs_set(True)
curses.echo()
file = stdscr.getstr(curses.LINES - 1, len(prompt) + 1).decode()
curses.curs_set(False)
curses.noecho()
try:
makedirs(dirname(abspath(file)), exist_ok=True)
with open(file, 'w') as f:
json.dump(entries, f)
except:
update_status_line(stdscr, mp)
stdscr.addstr(curses.LINES - 1, 0,
_("'{}': Can't open file for writing").format(file),
curses.color_pair(1))
else:
update_status_line(stdscr, mp)
stdscr.addstr(curses.LINES - 1, 0, _("'{}' written").format(file))
c = stdscr.getch()
curses.nocbreak()

Binary file not shown.

View File

@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: 2017-04-05 11:00+0700\n"
"PO-Revision-Date: 2017-04-05 11:05+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"
@ -17,50 +17,56 @@ msgstr ""
"Plural-Forms: nplurals=1; plural=0;\n"
"Language: vi_VN\n"
#: comp:42
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"
#: comp:43
msgid "play-all"
msgstr "chơi-tất-cả"
#: comp:44
msgid "play-selected"
msgstr "chơi-đã-chọn"
#: comp:45
msgid "repeat-current"
msgstr "lặp-một"
#: comp:46
msgid "repeat-all"
msgstr "lặp-tất-cả"
#: comp:47
msgid "repeat-selected"
msgstr "lặp-đã-chọn"
#: comp:48
msgid "shuffle-all"
msgstr "ngẫu-nhiên-tất-cả"
#: comp:49
msgid "shuffle-selected"
msgstr "ngẫu-nhiên-đã-chọn"
#: comp:144
msgid "URL"
msgstr "URL"
#: comp:145
msgid "Title"
msgstr "Tiêu đề"
#: comp:199
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"
#: comp:201
msgid "URL to an playlist on Youtube"
msgstr "URL của playlist trên Youtube"

Binary file not shown.

21
mpv.py
View File

@ -1,3 +1,18 @@
# python-mpv - Python interface to the awesome mpv media player
# Copyright (C) 2017 jaseg, Nguyễn Gia Phong
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from ctypes import *
import ctypes.util
@ -10,8 +25,6 @@ import collections
import re
import traceback
# vim: ts=4 sw=4 et
if os.name == 'nt':
backend = CDLL('mpv-1.dll')
fs_enc = 'utf-8'
@ -685,6 +698,10 @@ class MPV(object):
else:
raise TypeError('Cannot set {} property {} to value of type {}'.format(proptype, name, type(value)))
def _toggle_property(self, name):
"""Toggle a bool property."""
self._set_property(name, not self._get_property(name, bool), bool)
# Dict-like option access
def __getitem__(self, name, file_local=False):
""" Get an option value """

View File

@ -1,27 +1,29 @@
#!/usr/bin/env python3
from distutils.core import setup
from os.path import expanduser
from sys import prefix
with open('README.rst') as f:
long_description = f.read()
setup(name = 'comp', version = '0.1.1a1',
url = 'https://github.com/McSinyx/comp',
description = ('Curses Online Media Player'),
setup(name='comp', version='0.1.1a2',
url='https://github.com/McSinyx/comp',
description=('Curses Online Media Player'),
long_description=long_description,
author = 'McSinyx', author_email = 'vn.mcsinyx@gmail.com',
py_modules = ['mpv'], scripts=['comp'],
data_files=[('/etc/comp', ['settings.ini'])],
classifiers = [
author='Nguyễn Gia Phong', author_email='vn.mcsinyx@gmail.com',
py_modules=['mpv'], scripts=['comp'],
data_files=[
('{}/share/locale/vi/LC_MESSAGES/'.format(prefix), ['locale/vi/LC_MESSAGES/comp.mo']),
('/etc/comp', ['settings.ini'])
], classifiers=[
'Development Status :: 3 - Alpha',
'Environment :: Console :: Curses',
'Intended Audience :: End Users/Desktop',
'License :: OSI Approved :: GNU Affero General Public License v3',
'Natural Language :: English',
'Natural Language :: Vietnamese', # planned
'Natural Language :: Vietnamese',
'Operating System :: POSIX',
'Programming Language :: Python :: 3.5',
'Topic :: Multimedia :: Sound/Audio :: Players',
'Topic :: Multimedia :: Video :: Display'
], license = 'AGPLv3')
], platforms=['POSIX'], license='AGPLv3')