Add save playlist function and complete Vietnamese translation
This commit is contained in:
parent
c224c24b84
commit
894fa77fd6
66
README.rst
66
README.rst
|
@ -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
126
comp
|
@ -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.
|
@ -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
21
mpv.py
|
@ -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 """
|
||||
|
|
22
setup.py
22
setup.py
|
@ -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')
|
||||
|
|
Loading…
Reference in New Issue