mirror of
https://github.com/vale981/clay
synced 2025-03-06 01:51:38 -05:00
Forward merge to master
This commit is contained in:
commit
b6605b5576
22 changed files with 514 additions and 877 deletions
31
.codeclimate.yml
Normal file
31
.codeclimate.yml
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
version: "2" # required to adjust maintainability checks
|
||||||
|
exclude_patterns:
|
||||||
|
- "clay/vlc.py"
|
||||||
|
engines:
|
||||||
|
duplication:
|
||||||
|
enabled: false
|
||||||
|
checks:
|
||||||
|
argument-count:
|
||||||
|
config:
|
||||||
|
threshold: 8 # maximum complexity for ~ B mark in Radon
|
||||||
|
complex-logic:
|
||||||
|
config:
|
||||||
|
threshold: 4
|
||||||
|
file-lines:
|
||||||
|
config:
|
||||||
|
threshold: 1000
|
||||||
|
method-complexity:
|
||||||
|
config:
|
||||||
|
threshold: 10
|
||||||
|
method-count:
|
||||||
|
config:
|
||||||
|
threshold: 40
|
||||||
|
method-lines:
|
||||||
|
config:
|
||||||
|
threshold: 40 # to fit in 1366x768 screen
|
||||||
|
nested-control-flow:
|
||||||
|
config:
|
||||||
|
threshold: 4
|
||||||
|
return-statements:
|
||||||
|
config:
|
||||||
|
threshold: 5
|
438
.pylintrc
438
.pylintrc
|
@ -1,430 +1,16 @@
|
||||||
[MASTER]
|
[pylint]
|
||||||
|
max-line-length = 100
|
||||||
|
max-args = 8
|
||||||
|
|
||||||
# A comma-separated list of package or module names from where C extensions may
|
[messages control]
|
||||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
disable =
|
||||||
# run arbitrary code
|
too-few-public-methods,
|
||||||
extension-pkg-whitelist=
|
too-many-public-methods,
|
||||||
|
too-many-instance-attributes,
|
||||||
|
no-self-use,
|
||||||
|
too-many-ancestors
|
||||||
|
|
||||||
# Add files or directories to the blacklist. They should be base names, not
|
|
||||||
# paths.
|
|
||||||
ignore=CVS
|
|
||||||
|
|
||||||
# Add files or directories matching the regex patterns to the blacklist. The
|
|
||||||
# regex matches against base names, not paths.
|
|
||||||
ignore-patterns=
|
|
||||||
|
|
||||||
# Python code to execute, usually for sys.path manipulation such as
|
|
||||||
# pygtk.require().
|
|
||||||
#init-hook=
|
|
||||||
|
|
||||||
# Use multiple processes to speed up Pylint.
|
|
||||||
jobs=1
|
|
||||||
|
|
||||||
# List of plugins (as comma separated values of python modules names) to load,
|
|
||||||
# usually to register additional checkers.
|
|
||||||
load-plugins=
|
|
||||||
|
|
||||||
# Pickle collected data for later comparisons.
|
|
||||||
persistent=yes
|
|
||||||
|
|
||||||
# Specify a configuration file.
|
|
||||||
#rcfile=
|
|
||||||
|
|
||||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
|
||||||
# active Python interpreter and may run arbitrary code.
|
|
||||||
unsafe-load-any-extension=no
|
|
||||||
|
|
||||||
|
|
||||||
[MESSAGES CONTROL]
|
|
||||||
|
|
||||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
|
||||||
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
|
|
||||||
confidence=
|
|
||||||
|
|
||||||
# Disable the message, report, category or checker with the given id(s). You
|
|
||||||
# can either give multiple identifiers separated by comma (,) or put this
|
|
||||||
# option multiple times (only on the command line, not in the configuration
|
|
||||||
# file where it should appear only once).You can also use "--disable=all" to
|
|
||||||
# disable everything first and then reenable specific checks. For example, if
|
|
||||||
# you want to run only the similarities checker, you can use "--disable=all
|
|
||||||
# --enable=similarities". If you want to run only the classes checker, but have
|
|
||||||
# no Warning level messages displayed, use"--disable=all --enable=classes
|
|
||||||
# --disable=W"
|
|
||||||
# disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call
|
|
||||||
|
|
||||||
# Enable the message, report, category or checker with the given id(s). You can
|
|
||||||
# either give multiple identifier separated by comma (,) or put this option
|
|
||||||
# multiple time (only on the command line, not in the configuration file where
|
|
||||||
# it should appear only once). See also the "--disable" option for examples.
|
|
||||||
enable=
|
|
||||||
|
|
||||||
|
|
||||||
[REPORTS]
|
|
||||||
|
|
||||||
# Python expression which should return a note less than 10 (10 is the highest
|
|
||||||
# note). You have access to the variables errors warning, statement which
|
|
||||||
# respectively contain the number of errors / warnings messages and the total
|
|
||||||
# number of statements analyzed. This is used by the global evaluation report
|
|
||||||
# (RP0004).
|
|
||||||
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
|
||||||
|
|
||||||
# Template used to display messages. This is a python new-style format string
|
|
||||||
# used to format the message information. See doc for all details
|
|
||||||
#msg-template=
|
|
||||||
|
|
||||||
# Set the output format. Available formats are text, parseable, colorized, json
|
|
||||||
# and msvs (visual studio).You can also give a reporter class, eg
|
|
||||||
# mypackage.mymodule.MyReporterClass.
|
|
||||||
output-format=text
|
|
||||||
|
|
||||||
# Tells whether to display a full report or only the messages
|
|
||||||
reports=no
|
|
||||||
|
|
||||||
# Activate the evaluation score.
|
|
||||||
score=yes
|
|
||||||
|
|
||||||
|
|
||||||
[REFACTORING]
|
|
||||||
|
|
||||||
# Maximum number of nested blocks for function / method body
|
|
||||||
max-nested-blocks=5
|
|
||||||
|
|
||||||
|
|
||||||
[VARIABLES]
|
|
||||||
|
|
||||||
# List of additional names supposed to be defined in builtins. Remember that
|
|
||||||
# you should avoid to define new builtins when possible.
|
|
||||||
additional-builtins=
|
|
||||||
|
|
||||||
# Tells whether unused global variables should be treated as a violation.
|
|
||||||
allow-global-unused-variables=yes
|
|
||||||
|
|
||||||
# List of strings which can identify a callback function by name. A callback
|
|
||||||
# name must start or end with one of those strings.
|
|
||||||
callbacks=cb_,_cb
|
|
||||||
|
|
||||||
# A regular expression matching the name of dummy variables (i.e. expectedly
|
|
||||||
# not used).
|
|
||||||
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
|
||||||
|
|
||||||
# Argument names that match this expression will be ignored. Default to name
|
|
||||||
# with leading underscore
|
|
||||||
ignored-argument-names=_.*|^ignored_|^unused_
|
|
||||||
|
|
||||||
# Tells whether we should check for unused import in __init__ files.
|
|
||||||
init-import=no
|
|
||||||
|
|
||||||
# List of qualified module names which can have objects that can redefine
|
|
||||||
# builtins.
|
|
||||||
redefining-builtins-modules=six.moves,future.builtins
|
|
||||||
|
|
||||||
|
|
||||||
[TYPECHECK]
|
|
||||||
|
|
||||||
# List of decorators that produce context managers, such as
|
|
||||||
# contextlib.contextmanager. Add to this list to register other decorators that
|
|
||||||
# produce valid context managers.
|
|
||||||
contextmanager-decorators=contextlib.contextmanager
|
|
||||||
|
|
||||||
# List of members which are set dynamically and missed by pylint inference
|
|
||||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
|
||||||
# expressions are accepted.
|
|
||||||
generated-members=
|
|
||||||
|
|
||||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
|
||||||
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
|
||||||
ignore-mixin-members=yes
|
|
||||||
|
|
||||||
# This flag controls whether pylint should warn about no-member and similar
|
|
||||||
# checks whenever an opaque object is returned when inferring. The inference
|
|
||||||
# can return multiple potential results while evaluating a Python object, but
|
|
||||||
# some branches might not be evaluated, which results in partial inference. In
|
|
||||||
# that case, it might be useful to still emit no-member and other checks for
|
|
||||||
# the rest of the inferred objects.
|
|
||||||
ignore-on-opaque-inference=yes
|
|
||||||
|
|
||||||
# List of class names for which member attributes should not be checked (useful
|
|
||||||
# for classes with dynamically set attributes). This supports the use of
|
|
||||||
# qualified names.
|
|
||||||
ignored-classes=optparse.Values,thread._local,_thread._local
|
|
||||||
|
|
||||||
# List of module names for which member attributes should not be checked
|
|
||||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
|
||||||
# and thus existing member attributes cannot be deduced by static analysis. It
|
|
||||||
# supports qualified module names, as well as Unix pattern matching.
|
|
||||||
ignored-modules=
|
|
||||||
|
|
||||||
# Show a hint with possible names when a member name was not found. The aspect
|
|
||||||
# of finding the hint is based on edit distance.
|
|
||||||
missing-member-hint=yes
|
|
||||||
|
|
||||||
# The minimum edit distance a name should have in order to be considered a
|
|
||||||
# similar match for a missing member name.
|
|
||||||
missing-member-hint-distance=1
|
|
||||||
|
|
||||||
# The total number of similar names that should be taken in consideration when
|
|
||||||
# showing a hint for a missing member.
|
|
||||||
missing-member-max-choices=1
|
|
||||||
|
|
||||||
|
|
||||||
[BASIC]
|
|
||||||
|
|
||||||
# Naming hint for argument names
|
|
||||||
argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
|
|
||||||
|
|
||||||
# Regular expression matching correct argument names
|
|
||||||
argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
|
|
||||||
|
|
||||||
# Naming hint for attribute names
|
|
||||||
attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
|
|
||||||
|
|
||||||
# Regular expression matching correct attribute names
|
|
||||||
attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
|
|
||||||
|
|
||||||
# Bad variable names which should always be refused, separated by a comma
|
|
||||||
bad-names=foo,bar,baz,toto,tutu,tata
|
|
||||||
|
|
||||||
# Naming hint for class attribute names
|
|
||||||
class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
|
|
||||||
|
|
||||||
# Regular expression matching correct class attribute names
|
|
||||||
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
|
|
||||||
|
|
||||||
# Naming hint for class names
|
|
||||||
class-name-hint=[A-Z_][a-zA-Z0-9]+$
|
|
||||||
|
|
||||||
# Regular expression matching correct class names
|
|
||||||
class-rgx=[A-Z_][a-zA-Z0-9]+$
|
|
||||||
|
|
||||||
# Naming hint for constant names
|
|
||||||
const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
|
|
||||||
|
|
||||||
# Regular expression matching correct constant names
|
|
||||||
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
|
|
||||||
|
|
||||||
# Minimum line length for functions/classes that require docstrings, shorter
|
|
||||||
# ones are exempt.
|
|
||||||
docstring-min-length=-1
|
|
||||||
|
|
||||||
# Naming hint for function names
|
|
||||||
function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
|
|
||||||
|
|
||||||
# Regular expression matching correct function names
|
|
||||||
function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
|
|
||||||
|
|
||||||
# Good variable names which should always be accepted, separated by a comma
|
|
||||||
good-names=i,j,k,ex,Run,_
|
|
||||||
|
|
||||||
# Include a hint for the correct naming format with invalid-name
|
|
||||||
include-naming-hint=no
|
|
||||||
|
|
||||||
# Naming hint for inline iteration names
|
|
||||||
inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
|
|
||||||
|
|
||||||
# Regular expression matching correct inline iteration names
|
|
||||||
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
|
|
||||||
|
|
||||||
# Naming hint for method names
|
|
||||||
method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
|
|
||||||
|
|
||||||
# Regular expression matching correct method names
|
|
||||||
method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
|
|
||||||
|
|
||||||
# Naming hint for module names
|
|
||||||
module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
|
|
||||||
|
|
||||||
# Regular expression matching correct module names
|
|
||||||
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
|
|
||||||
|
|
||||||
# Colon-delimited sets of names that determine each other's naming style when
|
|
||||||
# the name regexes allow several styles.
|
|
||||||
name-group=
|
|
||||||
|
|
||||||
# Regular expression which should only match function or class names that do
|
|
||||||
# not require a docstring.
|
|
||||||
no-docstring-rgx=^_
|
|
||||||
|
|
||||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
|
||||||
# to this list to register other decorators that produce valid properties.
|
|
||||||
property-classes=abc.abstractproperty
|
|
||||||
|
|
||||||
# Naming hint for variable names
|
|
||||||
variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
|
|
||||||
|
|
||||||
# Regular expression matching correct variable names
|
|
||||||
variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
|
|
||||||
|
|
||||||
|
|
||||||
[FORMAT]
|
|
||||||
|
|
||||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
|
||||||
expected-line-ending-format=
|
|
||||||
|
|
||||||
# Regexp for a line that is allowed to be longer than the limit.
|
|
||||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
|
||||||
|
|
||||||
# Number of spaces of indent required inside a hanging or continued line.
|
|
||||||
indent-after-paren=4
|
|
||||||
|
|
||||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
|
||||||
# tab).
|
|
||||||
indent-string=' '
|
|
||||||
|
|
||||||
# Maximum number of characters on a single line.
|
|
||||||
max-line-length=100
|
|
||||||
|
|
||||||
# Maximum number of lines in a module
|
|
||||||
max-module-lines=1000
|
|
||||||
|
|
||||||
# List of optional constructs for which whitespace checking is disabled. `dict-
|
|
||||||
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
|
|
||||||
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
|
|
||||||
# `empty-line` allows space-only lines.
|
|
||||||
no-space-check=trailing-comma,dict-separator
|
|
||||||
|
|
||||||
# Allow the body of a class to be on the same line as the declaration if body
|
|
||||||
# contains single statement.
|
|
||||||
single-line-class-stmt=no
|
|
||||||
|
|
||||||
# Allow the body of an if to be on the same line as the test if there is no
|
|
||||||
# else.
|
|
||||||
single-line-if-stmt=no
|
|
||||||
|
|
||||||
|
|
||||||
[SIMILARITIES]
|
|
||||||
|
|
||||||
# Ignore comments when computing similarities.
|
|
||||||
ignore-comments=yes
|
|
||||||
|
|
||||||
# Ignore docstrings when computing similarities.
|
|
||||||
ignore-docstrings=yes
|
|
||||||
|
|
||||||
# Ignore imports when computing similarities.
|
|
||||||
ignore-imports=yes
|
|
||||||
|
|
||||||
# Minimum lines number of a similarity.
|
|
||||||
min-similarity-lines=4
|
|
||||||
|
|
||||||
|
|
||||||
[MISCELLANEOUS]
|
|
||||||
|
|
||||||
|
[miscellaneous]
|
||||||
# List of note tags to take in consideration, separated by a comma.
|
# List of note tags to take in consideration, separated by a comma.
|
||||||
notes=FIXME,XXX
|
notes=
|
||||||
|
|
||||||
|
|
||||||
[SPELLING]
|
|
||||||
|
|
||||||
# Spelling dictionary name. Available dictionaries: en_GH (hunspell), en_IE
|
|
||||||
# (hunspell), en_BW (hunspell), en_HK (hunspell), en_TT (hunspell), en_US
|
|
||||||
# (hunspell), en_BZ (hunspell), en_NA (hunspell), he (hspell), uk (aspell),
|
|
||||||
# en_NG (hunspell), en (aspell), en_CA (aspell), en_JM (hunspell), en_PH
|
|
||||||
# (hunspell), en_AG (hunspell), en_IN (hunspell), en_ZW (hunspell), en_SG
|
|
||||||
# (hunspell), en_GB (hunspell), en_DK (hunspell), en_ZA (hunspell), en_NZ
|
|
||||||
# (hunspell), en_BS (hunspell).
|
|
||||||
spelling-dict=
|
|
||||||
|
|
||||||
# List of comma separated words that should not be checked.
|
|
||||||
spelling-ignore-words=
|
|
||||||
|
|
||||||
# A path to a file that contains private dictionary; one word per line.
|
|
||||||
spelling-private-dict-file=
|
|
||||||
|
|
||||||
# Tells whether to store unknown words to indicated private dictionary in
|
|
||||||
# --spelling-private-dict-file option instead of raising a message.
|
|
||||||
spelling-store-unknown-words=no
|
|
||||||
|
|
||||||
|
|
||||||
[LOGGING]
|
|
||||||
|
|
||||||
# Logging modules to check that the string format arguments are in logging
|
|
||||||
# function parameter format
|
|
||||||
logging-modules=logging
|
|
||||||
|
|
||||||
|
|
||||||
[DESIGN]
|
|
||||||
|
|
||||||
# Maximum number of arguments for function / method
|
|
||||||
max-args=5
|
|
||||||
|
|
||||||
# Maximum number of attributes for a class (see R0902).
|
|
||||||
max-attributes=7
|
|
||||||
|
|
||||||
# Maximum number of boolean expressions in a if statement
|
|
||||||
max-bool-expr=5
|
|
||||||
|
|
||||||
# Maximum number of branch for function / method body
|
|
||||||
max-branches=12
|
|
||||||
|
|
||||||
# Maximum number of locals for function / method body
|
|
||||||
max-locals=15
|
|
||||||
|
|
||||||
# Maximum number of parents for a class (see R0901).
|
|
||||||
max-parents=7
|
|
||||||
|
|
||||||
# Maximum number of public methods for a class (see R0904).
|
|
||||||
max-public-methods=20
|
|
||||||
|
|
||||||
# Maximum number of return / yield for function / method body
|
|
||||||
max-returns=6
|
|
||||||
|
|
||||||
# Maximum number of statements in function / method body
|
|
||||||
max-statements=50
|
|
||||||
|
|
||||||
# Minimum number of public methods for a class (see R0903).
|
|
||||||
min-public-methods=2
|
|
||||||
|
|
||||||
|
|
||||||
[CLASSES]
|
|
||||||
|
|
||||||
# List of method names used to declare (i.e. assign) instance attributes.
|
|
||||||
defining-attr-methods=__init__,__new__,setUp
|
|
||||||
|
|
||||||
# List of member names, which should be excluded from the protected access
|
|
||||||
# warning.
|
|
||||||
exclude-protected=_asdict,_fields,_replace,_source,_make
|
|
||||||
|
|
||||||
# List of valid names for the first argument in a class method.
|
|
||||||
valid-classmethod-first-arg=cls
|
|
||||||
|
|
||||||
# List of valid names for the first argument in a metaclass class method.
|
|
||||||
valid-metaclass-classmethod-first-arg=mcs
|
|
||||||
|
|
||||||
|
|
||||||
[IMPORTS]
|
|
||||||
|
|
||||||
# Allow wildcard imports from modules that define __all__.
|
|
||||||
allow-wildcard-with-all=no
|
|
||||||
|
|
||||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
|
||||||
# 3 compatible code, which means that the block might have code that exists
|
|
||||||
# only in one or another interpreter, leading to false positives when analysed.
|
|
||||||
analyse-fallback-blocks=no
|
|
||||||
|
|
||||||
# Deprecated modules which should not be used, separated by a comma
|
|
||||||
deprecated-modules=optparse,tkinter.tix
|
|
||||||
|
|
||||||
# Create a graph of external dependencies in the given file (report RP0402 must
|
|
||||||
# not be disabled)
|
|
||||||
ext-import-graph=
|
|
||||||
|
|
||||||
# Create a graph of every (i.e. internal and external) dependencies in the
|
|
||||||
# given file (report RP0402 must not be disabled)
|
|
||||||
import-graph=
|
|
||||||
|
|
||||||
# Create a graph of internal dependencies in the given file (report RP0402 must
|
|
||||||
# not be disabled)
|
|
||||||
int-import-graph=
|
|
||||||
|
|
||||||
# Force import order to recognize a module as part of the standard
|
|
||||||
# compatibility libraries.
|
|
||||||
known-standard-library=
|
|
||||||
|
|
||||||
# Force import order to recognize a module as part of a third party library.
|
|
||||||
known-third-party=enchant
|
|
||||||
|
|
||||||
|
|
||||||
[EXCEPTIONS]
|
|
||||||
|
|
||||||
# Exceptions that will emit a warning when being caught. Defaults to
|
|
||||||
# "Exception"
|
|
||||||
overgeneral-exceptions=Exception
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ before_install:
|
||||||
- "sudo apt-get update"
|
- "sudo apt-get update"
|
||||||
- "sudo apt-get install python-gi python3-gi"
|
- "sudo apt-get install python-gi python3-gi"
|
||||||
install:
|
install:
|
||||||
- "pip install tox"
|
- "pip install tox radon"
|
||||||
script:
|
script:
|
||||||
- "tox"
|
- "tox"
|
||||||
|
|
||||||
|
|
3
Makefile
3
Makefile
|
@ -24,3 +24,6 @@ run: | build
|
||||||
docs:
|
docs:
|
||||||
make -C docs html
|
make -C docs html
|
||||||
|
|
||||||
|
check:
|
||||||
|
pylint clay --ignore-imports=y
|
||||||
|
radon cc -a -s -nC -e clay/vlc.py clay
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
|
|
||||||
# Clay [beta]
|
# Clay [beta]
|
||||||
|
|
||||||
[](https://travis-ci.org/and3rson/clay) [](http://clay.readthedocs.io/en/latest/?badge=latest)
|
[](https://travis-ci.org/and3rson/clay) [](http://clay.readthedocs.io/en/latest/?badge=latest) [](https://badge.fury.io/py/clay-player) [](https://codeclimate.com/github/and3rson/clay/maintainability)
|
||||||
|
|
||||||
Standalone command line player for Google Play Music.
|
Standalone command line player for Google Play Music.
|
||||||
|
|
||||||
|
@ -32,6 +32,11 @@ This project is neither affiliated nor endorsed by Google.
|
||||||
|
|
||||||
It's being actively developed, but is still in the early beta stage, so many features are missing and/or may be bugged.
|
It's being actively developed, but is still in the early beta stage, so many features are missing and/or may be bugged.
|
||||||
|
|
||||||
|
We're on IRC!
|
||||||
|
|
||||||
|
- Server: irc.oftc.net
|
||||||
|
- Channel: **#clay**
|
||||||
|
|
||||||
Screenshot:
|
Screenshot:
|
||||||
|
|
||||||

|

|
||||||
|
|
75
clay/app.py
75
clay/app.py
|
@ -1,7 +1,5 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# pylint: disable=wrong-import-position
|
# pylint: disable=wrong-import-position
|
||||||
# pylint: disable=too-many-instance-attributes
|
|
||||||
# pylint: disable=too-many-public-methods
|
|
||||||
"""
|
"""
|
||||||
Main app entrypoint.
|
Main app entrypoint.
|
||||||
"""
|
"""
|
||||||
|
@ -15,7 +13,7 @@ import os
|
||||||
import urwid
|
import urwid
|
||||||
|
|
||||||
from clay import meta
|
from clay import meta
|
||||||
from clay.player import Player
|
from clay.player import player
|
||||||
from clay.playbar import PlayBar
|
from clay.playbar import PlayBar
|
||||||
from clay.pages.debug import DebugPage
|
from clay.pages.debug import DebugPage
|
||||||
from clay.pages.mylibrary import MyLibraryPage
|
from clay.pages.mylibrary import MyLibraryPage
|
||||||
|
@ -23,18 +21,17 @@ from clay.pages.myplaylists import MyPlaylistsPage
|
||||||
from clay.pages.playerqueue import QueuePage
|
from clay.pages.playerqueue import QueuePage
|
||||||
from clay.pages.search import SearchPage
|
from clay.pages.search import SearchPage
|
||||||
from clay.pages.settings import SettingsPage
|
from clay.pages.settings import SettingsPage
|
||||||
from clay.settings import Settings
|
from clay.settings import settings
|
||||||
from clay.notifications import NotificationArea
|
from clay.notifications import notification_area
|
||||||
from clay.gp import GP
|
from clay.gp import gp
|
||||||
|
|
||||||
|
|
||||||
def create_palette(transparent=False):
|
def create_palette(transparent=False):
|
||||||
config = Settings.get_config('colours')
|
config = settings.get_section('colours')
|
||||||
|
|
||||||
return [(name, '', '', '', config[name]['foreground'], config[name]['background'])
|
return [(name, '', '', '', config[name]['foreground'], config[name]['background'])
|
||||||
for name in config]
|
for name in config]
|
||||||
|
|
||||||
|
|
||||||
class AppWidget(urwid.Frame):
|
class AppWidget(urwid.Frame):
|
||||||
"""
|
"""
|
||||||
Root widget.
|
Root widget.
|
||||||
|
@ -104,35 +101,25 @@ class AppWidget(urwid.Frame):
|
||||||
self.current_page = None
|
self.current_page = None
|
||||||
self.loop = None
|
self.loop = None
|
||||||
|
|
||||||
NotificationArea.set_app(self)
|
notification_area.set_app(self)
|
||||||
self._login_notification = None
|
self._login_notification = None
|
||||||
|
|
||||||
self._cancel_actions = []
|
self._cancel_actions = []
|
||||||
|
|
||||||
self.header = urwid.Pile([
|
self.header = urwid.Pile([
|
||||||
# urwid.Divider('\u2500'),
|
|
||||||
urwid.AttrWrap(urwid.Columns([
|
urwid.AttrWrap(urwid.Columns([
|
||||||
('pack', tab)
|
('pack', tab)
|
||||||
for tab
|
for tab
|
||||||
in self.tabs
|
in self.tabs
|
||||||
], dividechars=0), 'panel'),
|
], dividechars=0), 'panel'),
|
||||||
NotificationArea.get()
|
notification_area
|
||||||
# urwid.Divider('\u2500')
|
|
||||||
])
|
])
|
||||||
self.playbar = PlayBar(self)
|
self.playbar = PlayBar(self)
|
||||||
# self.panel = urwid.Pile([
|
|
||||||
# urwid.Columns([
|
|
||||||
# urwid.Divider(u'\u2500'),
|
|
||||||
# ]),
|
|
||||||
# self.playbar
|
|
||||||
# ])
|
|
||||||
# self.current_page = self.pages[0]
|
|
||||||
super(AppWidget, self).__init__(
|
super(AppWidget, self).__init__(
|
||||||
header=self.header,
|
header=self.header,
|
||||||
footer=self.playbar,
|
footer=self.playbar,
|
||||||
body=urwid.Filler(urwid.Text('Loading...', align='center'))
|
body=urwid.Filler(urwid.Text('Loading...', align='center'))
|
||||||
)
|
)
|
||||||
# self.current_page.activate()
|
|
||||||
|
|
||||||
self.set_page('MyLibraryPage')
|
self.set_page('MyLibraryPage')
|
||||||
self.log_in()
|
self.log_in()
|
||||||
|
@ -143,31 +130,30 @@ class AppWidget(urwid.Frame):
|
||||||
|
|
||||||
Request user authorization.
|
Request user authorization.
|
||||||
"""
|
"""
|
||||||
config = Settings.get_config('play_settings')
|
|
||||||
username, password, device_id, authtoken = [
|
username, password, device_id, authtoken = [
|
||||||
config.get(x)
|
settings.get(x)
|
||||||
for x
|
for x
|
||||||
in ('username', 'password', 'device_id', 'authtoken')
|
in ('username', 'password', 'device_id', 'authtoken')
|
||||||
]
|
]
|
||||||
if self._login_notification:
|
if self._login_notification:
|
||||||
self._login_notification.close()
|
self._login_notification.close()
|
||||||
if use_token and authtoken:
|
if use_token and authtoken:
|
||||||
self._login_notification = NotificationArea.notify('Using cached auth token...')
|
self._login_notification = notification_area.notify('Using cached auth token...')
|
||||||
GP.get().use_authtoken_async(
|
gp.use_authtoken_async(
|
||||||
authtoken,
|
authtoken,
|
||||||
device_id,
|
device_id,
|
||||||
callback=self.on_check_authtoken
|
callback=self.on_check_authtoken
|
||||||
)
|
)
|
||||||
elif username and password and device_id:
|
elif username and password and device_id:
|
||||||
self._login_notification = NotificationArea.notify('Logging in...')
|
self._login_notification = notification_area.notify('Logging in...')
|
||||||
GP.get().login_async(
|
gp.login_async(
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
device_id,
|
device_id,
|
||||||
callback=self.on_login
|
callback=self.on_login
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self._login_notification = NotificationArea.notify(
|
self._login_notification = notification_area.notify(
|
||||||
'Please set your credentials on the settings page.'
|
'Please set your credentials on the settings page.'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -205,7 +191,8 @@ class AppWidget(urwid.Frame):
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
Settings.set_config(dict(authtoken=GP.get().get_authtoken()))
|
with settings.edit() as config:
|
||||||
|
config['authtoken'] = gp.get_authtoken()
|
||||||
|
|
||||||
self._login_notification.close()
|
self._login_notification.close()
|
||||||
|
|
||||||
|
@ -275,42 +262,41 @@ class AppWidget(urwid.Frame):
|
||||||
"""
|
"""
|
||||||
Seek to the start of the song.
|
Seek to the start of the song.
|
||||||
"""
|
"""
|
||||||
Player.get().seek_absolute(0)
|
player.seek_absolute(0)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def play_pause():
|
def play_pause():
|
||||||
"""
|
"""
|
||||||
Toggle play/pause.
|
Toggle play/pause.
|
||||||
"""
|
"""
|
||||||
Player.get().play_pause()
|
player.play_pause()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def next_song():
|
def next_song():
|
||||||
"""
|
"""
|
||||||
Play next song.
|
Play next song.
|
||||||
"""
|
"""
|
||||||
Player.get().next(True)
|
player.next(True)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def seek_backward():
|
def seek_backward():
|
||||||
"""
|
"""
|
||||||
Seek 5% backward.
|
Seek 5% backward.
|
||||||
"""
|
"""
|
||||||
Player.get().seek(-0.05)
|
player.seek(-0.05)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def seek_forward():
|
def seek_forward():
|
||||||
"""
|
"""
|
||||||
Seek 5% forward.
|
Seek 5% forward.
|
||||||
"""
|
"""
|
||||||
Player.get().seek(0.05)
|
player.seek(0.05)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def toggle_shuffle():
|
def toggle_shuffle():
|
||||||
"""
|
"""
|
||||||
Toggle random playback.
|
Toggle random playback.
|
||||||
"""
|
"""
|
||||||
player = Player.get()
|
|
||||||
player.set_random(not player.get_is_random())
|
player.set_random(not player.get_is_random())
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -318,7 +304,6 @@ class AppWidget(urwid.Frame):
|
||||||
"""
|
"""
|
||||||
Toggle repeat mode.
|
Toggle repeat mode.
|
||||||
"""
|
"""
|
||||||
player = Player.get()
|
|
||||||
player.set_repeat_one(not player.get_is_repeat_one())
|
player.set_repeat_one(not player.get_is_repeat_one())
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -335,7 +320,7 @@ class AppWidget(urwid.Frame):
|
||||||
try:
|
try:
|
||||||
action = self._cancel_actions.pop()
|
action = self._cancel_actions.pop()
|
||||||
except IndexError:
|
except IndexError:
|
||||||
NotificationArea.close_newest()
|
notification_area.close_newest()
|
||||||
else:
|
else:
|
||||||
action()
|
action()
|
||||||
|
|
||||||
|
@ -357,24 +342,30 @@ class MultilineVersionAction(argparse.Action):
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# This method is required to allow Clay to be ran as script via setuptools installation.
|
"""
|
||||||
# pylint: disable-all
|
Application entrypoint.
|
||||||
|
|
||||||
|
This method is required to allow Clay to be ran an application when installed via setuptools.
|
||||||
|
"""
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog=meta.APP_NAME,
|
prog=meta.APP_NAME,
|
||||||
description=meta.DESCRIPTION,
|
description=meta.DESCRIPTION,
|
||||||
epilog="This project is neither affiliated nor endorsed by Google.")
|
epilog="This project is neither affiliated nor endorsed by Google."
|
||||||
|
)
|
||||||
|
|
||||||
parser.add_argument("-v", "--version", action=MultilineVersionAction)
|
parser.add_argument("-v", "--version", action=MultilineVersionAction)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--with-x-keybinds",
|
"--with-x-keybinds",
|
||||||
help="define global X keybinds (requires Keybinder and PyGObject)",
|
help="define global X keybinds (requires Keybinder and PyGObject)",
|
||||||
action='store_true')
|
action='store_true'
|
||||||
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--transparent",
|
"--transparent",
|
||||||
help="use transparent background",
|
help="use transparent background",
|
||||||
action='store_true')
|
action='store_true'
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
@ -382,7 +373,7 @@ def main():
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
if args.with_x_keybinds:
|
if args.with_x_keybinds:
|
||||||
Player.get().enable_xorg_bindings()
|
player.enable_xorg_bindings()
|
||||||
|
|
||||||
# Run the actual program
|
# Run the actual program
|
||||||
app_widget = AppWidget()
|
app_widget = AppWidget()
|
||||||
|
|
|
@ -3,7 +3,7 @@ Clipboard utils.
|
||||||
"""
|
"""
|
||||||
from subprocess import Popen, PIPE
|
from subprocess import Popen, PIPE
|
||||||
|
|
||||||
from clay.notifications import NotificationArea
|
from clay.notifications import notification_area
|
||||||
|
|
||||||
|
|
||||||
COMMANDS = [
|
COMMANDS = [
|
||||||
|
@ -24,7 +24,7 @@ def copy(text):
|
||||||
if proc.returncode == 0:
|
if proc.returncode == 0:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
NotificationArea.notify(
|
notification_area.notify(
|
||||||
'Failed to copy text to clipboard. '
|
'Failed to copy text to clipboard. '
|
||||||
'Please install "xclip" or "xsel".'
|
'Please install "xclip" or "xsel".'
|
||||||
)
|
)
|
||||||
|
|
205
clay/gp.py
205
clay/gp.py
|
@ -14,7 +14,7 @@ from uuid import UUID
|
||||||
from gmusicapi.clients import Mobileclient
|
from gmusicapi.clients import Mobileclient
|
||||||
|
|
||||||
from clay.eventhook import EventHook
|
from clay.eventhook import EventHook
|
||||||
from clay.log import Logger
|
from clay.log import logger
|
||||||
|
|
||||||
|
|
||||||
def asynchronous(func):
|
def asynchronous(func):
|
||||||
|
@ -117,6 +117,13 @@ class Track(object):
|
||||||
return self.library_id
|
return self.library_id
|
||||||
return self.store_id
|
return self.store_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filename(self):
|
||||||
|
"""
|
||||||
|
Return a filename for this track.
|
||||||
|
"""
|
||||||
|
return self.store_id + '.mp3'
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return (
|
return (
|
||||||
(self.library_id and self.library_id == other.library_id) or
|
(self.library_id and self.library_id == other.library_id) or
|
||||||
|
@ -124,6 +131,100 @@ class Track(object):
|
||||||
(self.playlist_item_id and self.playlist_item_id == other.playlist_item_id)
|
(self.playlist_item_id and self.playlist_item_id == other.playlist_item_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_search(cls, data):
|
||||||
|
"""
|
||||||
|
Create track from search result data.
|
||||||
|
"""
|
||||||
|
# Data contains a nested track representation.
|
||||||
|
return Track(
|
||||||
|
title=data['track']['title'],
|
||||||
|
artist=data['track']['artist'],
|
||||||
|
duration=int(data['track']['durationMillis']),
|
||||||
|
source=cls.SOURCE_SEARCH,
|
||||||
|
store_id=data['track']['storeId'], # or data['trackId']
|
||||||
|
album_name=data['track']['album'],
|
||||||
|
album_url=data['track']['albumArtRef'][0]['url'],
|
||||||
|
original_data=data
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_station(cls, data):
|
||||||
|
"""
|
||||||
|
Create track from station track data.
|
||||||
|
"""
|
||||||
|
# Station tracks have all the info in place.
|
||||||
|
return Track(
|
||||||
|
title=data['title'],
|
||||||
|
artist=data['artist'],
|
||||||
|
duration=int(data['durationMillis']),
|
||||||
|
source=cls.SOURCE_STATION,
|
||||||
|
store_id=data['storeId'],
|
||||||
|
album_name=data['album'],
|
||||||
|
album_url=data['albumArtRef'][0]['url'],
|
||||||
|
original_data=data
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_library(cls, data):
|
||||||
|
"""
|
||||||
|
Create track from library track data.
|
||||||
|
"""
|
||||||
|
# Data contains all info about track
|
||||||
|
# including ID in library and ID in store.
|
||||||
|
UUID(data['id'])
|
||||||
|
return Track(
|
||||||
|
title=data['title'],
|
||||||
|
artist=data['artist'],
|
||||||
|
duration=int(data['durationMillis']),
|
||||||
|
source=cls.SOURCE_LIBRARY,
|
||||||
|
store_id=data['storeId'],
|
||||||
|
library_id=data['id'],
|
||||||
|
album_name=data['album'],
|
||||||
|
album_url=data['albumArtRef'][0]['url'],
|
||||||
|
original_data=data
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_playlist(cls, data):
|
||||||
|
"""
|
||||||
|
Create track from playlist track data.
|
||||||
|
"""
|
||||||
|
if 'track' in data:
|
||||||
|
# Data contains a nested track representation that can be used
|
||||||
|
# to construct new track.
|
||||||
|
return Track(
|
||||||
|
title=data['track']['title'],
|
||||||
|
artist=data['track']['artist'],
|
||||||
|
duration=int(data['track']['durationMillis']),
|
||||||
|
source=cls.SOURCE_PLAYLIST,
|
||||||
|
store_id=data['track']['storeId'], # or data['trackId']
|
||||||
|
playlist_item_id=data['id'],
|
||||||
|
album_name=data['track']['album'],
|
||||||
|
album_url=data['track']['albumArtRef'][0]['url'],
|
||||||
|
original_data=data
|
||||||
|
)
|
||||||
|
# We need to find a track in Library by trackId.
|
||||||
|
UUID(data['trackId'])
|
||||||
|
track = gp.get_track_by_id(data['trackId'])
|
||||||
|
return Track(
|
||||||
|
title=track.title,
|
||||||
|
artist=track.artist,
|
||||||
|
duration=track.duration,
|
||||||
|
source=cls.SOURCE_PLAYLIST,
|
||||||
|
store_id=track.store_id,
|
||||||
|
album_name=track.album_name,
|
||||||
|
album_url=track.album_url,
|
||||||
|
original_data=data
|
||||||
|
)
|
||||||
|
|
||||||
|
_CREATE_TRACK = {
|
||||||
|
SOURCE_SEARCH: '_from_search',
|
||||||
|
SOURCE_STATION: '_from_station',
|
||||||
|
SOURCE_LIBRARY: '_from_library',
|
||||||
|
SOURCE_PLAYLIST: '_from_playlist',
|
||||||
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_data(cls, data, source, many=False):
|
def from_data(cls, data, source, many=False):
|
||||||
"""
|
"""
|
||||||
|
@ -139,75 +240,9 @@ class Track(object):
|
||||||
]
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if source == Track.SOURCE_SEARCH:
|
return getattr(cls, cls._CREATE_TRACK[source])(data)
|
||||||
# Data contains a nested track representation.
|
|
||||||
return Track(
|
|
||||||
title=data['track']['title'],
|
|
||||||
artist=data['track']['artist'],
|
|
||||||
duration=int(data['track']['durationMillis']),
|
|
||||||
source=source,
|
|
||||||
store_id=data['track']['storeId'], # or data['trackId']
|
|
||||||
album_name=data['track']['album'],
|
|
||||||
album_url=data['track']['albumArtRef'][0]['url'],
|
|
||||||
original_data=data
|
|
||||||
)
|
|
||||||
elif source == Track.SOURCE_STATION:
|
|
||||||
# Station tracks have all the info in place.
|
|
||||||
return Track(
|
|
||||||
title=data['title'],
|
|
||||||
artist=data['artist'],
|
|
||||||
duration=int(data['durationMillis']),
|
|
||||||
source=source,
|
|
||||||
store_id=data['storeId'],
|
|
||||||
album_name=data['album'],
|
|
||||||
album_url=data['albumArtRef'][0]['url'],
|
|
||||||
original_data=data
|
|
||||||
)
|
|
||||||
elif source == Track.SOURCE_LIBRARY:
|
|
||||||
# Data contains all info about track
|
|
||||||
# including ID in library and ID in store.
|
|
||||||
UUID(data['id'])
|
|
||||||
return Track(
|
|
||||||
title=data['title'],
|
|
||||||
artist=data['artist'],
|
|
||||||
duration=int(data['durationMillis']),
|
|
||||||
source=source,
|
|
||||||
store_id=data['storeId'],
|
|
||||||
library_id=data['id'],
|
|
||||||
album_name=data['album'],
|
|
||||||
album_url=data['albumArtRef'][0]['url'],
|
|
||||||
original_data=data
|
|
||||||
)
|
|
||||||
elif source == Track.SOURCE_PLAYLIST:
|
|
||||||
if 'track' in data:
|
|
||||||
# Data contains a nested track representation that can be used
|
|
||||||
# to construct new track.
|
|
||||||
return Track(
|
|
||||||
title=data['track']['title'],
|
|
||||||
artist=data['track']['artist'],
|
|
||||||
duration=int(data['track']['durationMillis']),
|
|
||||||
source=source,
|
|
||||||
store_id=data['track']['storeId'], # or data['trackId']
|
|
||||||
playlist_item_id=data['id'],
|
|
||||||
album_name=data['track']['album'],
|
|
||||||
album_url=data['track']['albumArtRef'][0]['url'],
|
|
||||||
original_data=data
|
|
||||||
)
|
|
||||||
# We need to find a track in Library by trackId.
|
|
||||||
UUID(data['trackId'])
|
|
||||||
track = GP.get().get_track_by_id(data['trackId'])
|
|
||||||
return Track(
|
|
||||||
title=track.title,
|
|
||||||
artist=track.artist,
|
|
||||||
duration=track.duration,
|
|
||||||
source=source,
|
|
||||||
store_id=track.store_id,
|
|
||||||
album_name=track.album_name,
|
|
||||||
album_url=track.album_url,
|
|
||||||
original_data=data
|
|
||||||
)
|
|
||||||
except Exception as error: # pylint: disable=bare-except
|
except Exception as error: # pylint: disable=bare-except
|
||||||
Logger.get().error(
|
logger.error(
|
||||||
'Failed to parse track data: %s, failing data: %s',
|
'Failed to parse track data: %s, failing data: %s',
|
||||||
repr(error),
|
repr(error),
|
||||||
data
|
data
|
||||||
|
@ -241,11 +276,11 @@ class Track(object):
|
||||||
self.cached_url = url
|
self.cached_url = url
|
||||||
callback(url, error, self)
|
callback(url, error, self)
|
||||||
|
|
||||||
if GP.get().is_subscribed:
|
if gp.is_subscribed:
|
||||||
track_id = self.store_id
|
track_id = self.store_id
|
||||||
else:
|
else:
|
||||||
track_id = self.library_id
|
track_id = self.library_id
|
||||||
GP.get().get_stream_url_async(track_id, callback=on_get_url)
|
gp.get_stream_url_async(track_id, callback=on_get_url)
|
||||||
|
|
||||||
@synchronized
|
@synchronized
|
||||||
def create_station(self):
|
def create_station(self):
|
||||||
|
@ -254,7 +289,7 @@ class Track(object):
|
||||||
|
|
||||||
Returns :class:`.Station` instance.
|
Returns :class:`.Station` instance.
|
||||||
"""
|
"""
|
||||||
station_id = GP.get().mobile_client.create_station(
|
station_id = gp.mobile_client.create_station(
|
||||||
name=u'Station - {}'.format(self.title),
|
name=u'Station - {}'.format(self.title),
|
||||||
track_id=self.store_id
|
track_id=self.store_id
|
||||||
)
|
)
|
||||||
|
@ -268,7 +303,7 @@ class Track(object):
|
||||||
"""
|
"""
|
||||||
Add a track to my library.
|
Add a track to my library.
|
||||||
"""
|
"""
|
||||||
return GP.get().add_to_my_library(self)
|
return gp.add_to_my_library(self)
|
||||||
|
|
||||||
add_to_my_library_async = asynchronous(add_to_my_library)
|
add_to_my_library_async = asynchronous(add_to_my_library)
|
||||||
|
|
||||||
|
@ -276,7 +311,7 @@ class Track(object):
|
||||||
"""
|
"""
|
||||||
Remove a track from my library.
|
Remove a track from my library.
|
||||||
"""
|
"""
|
||||||
return GP.get().remove_from_my_library(self)
|
return gp.remove_from_my_library(self)
|
||||||
|
|
||||||
remove_from_my_library_async = asynchronous(remove_from_my_library)
|
remove_from_my_library_async = asynchronous(remove_from_my_library)
|
||||||
|
|
||||||
|
@ -341,7 +376,7 @@ class Station(object):
|
||||||
Fetch tracks related to this station and
|
Fetch tracks related to this station and
|
||||||
populate it with :class:`Track` instances.
|
populate it with :class:`Track` instances.
|
||||||
"""
|
"""
|
||||||
data = GP.get().mobile_client.get_station_tracks(self.id, 100)
|
data = gp.mobile_client.get_station_tracks(self.id, 100)
|
||||||
self._tracks = Track.from_data(data, Track.SOURCE_STATION, many=True)
|
self._tracks = Track.from_data(data, Track.SOURCE_STATION, many=True)
|
||||||
self._tracks_loaded = True
|
self._tracks_loaded = True
|
||||||
|
|
||||||
|
@ -420,7 +455,7 @@ class Playlist(object):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class GP(object):
|
class _GP(object):
|
||||||
"""
|
"""
|
||||||
Interface to :class:`gmusicapi.Mobileclient`. Implements
|
Interface to :class:`gmusicapi.Mobileclient`. Implements
|
||||||
asynchronous API calls, caching and some other perks.
|
asynchronous API calls, caching and some other perks.
|
||||||
|
@ -428,12 +463,9 @@ class GP(object):
|
||||||
Singleton.
|
Singleton.
|
||||||
"""
|
"""
|
||||||
# TODO: Switch to urwid signals for more explicitness?
|
# TODO: Switch to urwid signals for more explicitness?
|
||||||
instance = None
|
|
||||||
|
|
||||||
caches_invalidated = EventHook()
|
caches_invalidated = EventHook()
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
assert self.__class__.instance is None, 'Can be created only once!'
|
|
||||||
# self.is_debug = os.getenv('CLAY_DEBUG')
|
# self.is_debug = os.getenv('CLAY_DEBUG')
|
||||||
self.mobile_client = Mobileclient()
|
self.mobile_client = Mobileclient()
|
||||||
self.mobile_client._make_call = self._make_call_proxy(
|
self.mobile_client._make_call = self._make_call_proxy(
|
||||||
|
@ -449,16 +481,6 @@ class GP(object):
|
||||||
|
|
||||||
self.auth_state_changed = EventHook()
|
self.auth_state_changed = EventHook()
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get(cls):
|
|
||||||
"""
|
|
||||||
Create new :class:`.GP` instance or return existing one.
|
|
||||||
"""
|
|
||||||
if cls.instance is None:
|
|
||||||
cls.instance = GP()
|
|
||||||
|
|
||||||
return cls.instance
|
|
||||||
|
|
||||||
def _make_call_proxy(self, func):
|
def _make_call_proxy(self, func):
|
||||||
"""
|
"""
|
||||||
Return a function that wraps *fn* and logs args & return values.
|
Return a function that wraps *fn* and logs args & return values.
|
||||||
|
@ -467,7 +489,7 @@ class GP(object):
|
||||||
"""
|
"""
|
||||||
Wrapper function.
|
Wrapper function.
|
||||||
"""
|
"""
|
||||||
Logger.get().debug('GP::{}(*{}, **{})'.format(
|
logger.debug('GP::{}(*{}, **{})'.format(
|
||||||
protocol.__name__,
|
protocol.__name__,
|
||||||
args,
|
args,
|
||||||
kwargs
|
kwargs
|
||||||
|
@ -633,3 +655,6 @@ class GP(object):
|
||||||
Return True if user is subscribed on Google Play Music, false otherwise.
|
Return True if user is subscribed on Google Play Music, false otherwise.
|
||||||
"""
|
"""
|
||||||
return self.mobile_client.is_subscribed
|
return self.mobile_client.is_subscribed
|
||||||
|
|
||||||
|
|
||||||
|
gp = _GP() # pylint: disable=invalid-name
|
||||||
|
|
|
@ -5,16 +5,19 @@ Requires "gi" package and "Gtk" & "Keybinder" modules.
|
||||||
# pylint: disable=broad-except
|
# pylint: disable=broad-except
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from clay.settings import Settings
|
from clay.settings import settings
|
||||||
from clay.eventhook import EventHook
|
from clay.eventhook import EventHook
|
||||||
from clay.notifications import NotificationArea
|
from clay.notifications import notification_area
|
||||||
from clay.log import Logger
|
from clay.log import logger
|
||||||
|
|
||||||
|
|
||||||
IS_INIT = False
|
IS_INIT = False
|
||||||
def report_error(error_msg):
|
|
||||||
|
|
||||||
|
def report_error(exc):
|
||||||
"Print an error message to the debug screen"
|
"Print an error message to the debug screen"
|
||||||
Logger.get().error("{0}: {1}".format(error.__class__.__name__, error_msg))
|
logger.error("{0}: {1}".format(exc.__class__.__name__, exc))
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
|
@ -36,7 +39,7 @@ else:
|
||||||
IS_INIT = True
|
IS_INIT = True
|
||||||
|
|
||||||
|
|
||||||
class HotkeyManager(object):
|
class _HotkeyManager(object):
|
||||||
"""
|
"""
|
||||||
Manages configs.
|
Manages configs.
|
||||||
Runs Gtk main loop in a thread.
|
Runs Gtk main loop in a thread.
|
||||||
|
@ -47,10 +50,7 @@ class HotkeyManager(object):
|
||||||
'prev': 'XF86AudioPrev'
|
'prev': 'XF86AudioPrev'
|
||||||
}
|
}
|
||||||
|
|
||||||
instance = None
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
assert self.__class__.instance is None, 'Can be created only once!'
|
|
||||||
self.hotkeys = {}
|
self.hotkeys = {}
|
||||||
self.config = None
|
self.config = None
|
||||||
|
|
||||||
|
@ -64,29 +64,20 @@ class HotkeyManager(object):
|
||||||
|
|
||||||
threading.Thread(target=Gtk.main).start()
|
threading.Thread(target=Gtk.main).start()
|
||||||
else:
|
else:
|
||||||
Logger.get().debug("Not loading the global shortcuts.")
|
logger.debug("Not loading the global shortcuts.")
|
||||||
NotificationArea.notify(ERROR_MESSAGE +
|
notification_area.notify(
|
||||||
", this means the global shortcuts will not work.\n" +
|
ERROR_MESSAGE +
|
||||||
"You can check the log for more details.")
|
", this means the global shortcuts will not work.\n" +
|
||||||
|
"You can check the log for more details."
|
||||||
@classmethod
|
)
|
||||||
def get(cls):
|
|
||||||
"""
|
|
||||||
Create new :class:`.HotkeyManager` instance or return existing one.
|
|
||||||
"""
|
|
||||||
if cls.instance is None:
|
|
||||||
cls.instance = HotkeyManager()
|
|
||||||
|
|
||||||
return cls.instance
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_keys():
|
def load_keys():
|
||||||
"""
|
"""
|
||||||
Load hotkey config from settings.
|
Load hotkey config from settings.
|
||||||
"""
|
"""
|
||||||
config = Settings.get_config('play_settings')
|
hotkeys = settings.get('hotkeys', default={})
|
||||||
hotkeys = config.get('hotkeys', {})
|
for operation, default_key in _HotkeyManager.DEFAULT_HOTKEYS.items():
|
||||||
for operation, default_key in HotkeyManager.DEFAULT_HOTKEYS.items():
|
|
||||||
if operation not in hotkeys or not hotkeys[operation]:
|
if operation not in hotkeys or not hotkeys[operation]:
|
||||||
hotkeys[operation] = default_key
|
hotkeys[operation] = default_key
|
||||||
return hotkeys
|
return hotkeys
|
||||||
|
@ -107,3 +98,6 @@ class HotkeyManager(object):
|
||||||
"""
|
"""
|
||||||
assert key
|
assert key
|
||||||
getattr(self, operation).fire()
|
getattr(self, operation).fire()
|
||||||
|
|
||||||
|
|
||||||
|
hotkey_manager = _HotkeyManager() # pylint: disable=invalid-name
|
||||||
|
|
22
clay/log.py
22
clay/log.py
|
@ -8,7 +8,7 @@ from datetime import datetime
|
||||||
from clay.eventhook import EventHook
|
from clay.eventhook import EventHook
|
||||||
|
|
||||||
|
|
||||||
class LoggerRecord(object):
|
class _LoggerRecord(object):
|
||||||
"""
|
"""
|
||||||
Represents a logger record.
|
Represents a logger record.
|
||||||
"""
|
"""
|
||||||
|
@ -40,17 +40,14 @@ class LoggerRecord(object):
|
||||||
return self._message % self._args
|
return self._message % self._args
|
||||||
|
|
||||||
|
|
||||||
class Logger(object):
|
class _Logger(object):
|
||||||
"""
|
"""
|
||||||
Global logger.
|
Global logger.
|
||||||
|
|
||||||
Allows subscribing to log events.
|
Allows subscribing to log events.
|
||||||
"""
|
"""
|
||||||
instance = None
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
assert self.__class__.instance is None, 'Can be created only once!'
|
|
||||||
|
|
||||||
self.logs = []
|
self.logs = []
|
||||||
self.logfile = open('/tmp/clay.log', 'w')
|
self.logfile = open('/tmp/clay.log', 'w')
|
||||||
|
|
||||||
|
@ -58,23 +55,13 @@ class Logger(object):
|
||||||
|
|
||||||
self.on_log_event = EventHook()
|
self.on_log_event = EventHook()
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get(cls):
|
|
||||||
"""
|
|
||||||
Create new :class:`.Logger` instance or return existing one.
|
|
||||||
"""
|
|
||||||
if cls.instance is None:
|
|
||||||
cls.instance = Logger()
|
|
||||||
|
|
||||||
return cls.instance
|
|
||||||
|
|
||||||
def log(self, level, message, *args):
|
def log(self, level, message, *args):
|
||||||
"""
|
"""
|
||||||
Add log item.
|
Add log item.
|
||||||
"""
|
"""
|
||||||
self._lock.acquire()
|
self._lock.acquire()
|
||||||
try:
|
try:
|
||||||
logger_record = LoggerRecord(level, message, args)
|
logger_record = _LoggerRecord(level, message, args)
|
||||||
self.logs.append(logger_record)
|
self.logs.append(logger_record)
|
||||||
self.logfile.write('{} {:8} {}\n'.format(
|
self.logfile.write('{} {:8} {}\n'.format(
|
||||||
logger_record.formatted_timestamp,
|
logger_record.formatted_timestamp,
|
||||||
|
@ -117,3 +104,6 @@ class Logger(object):
|
||||||
Return all logs.
|
Return all logs.
|
||||||
"""
|
"""
|
||||||
return self.logs
|
return self.logs
|
||||||
|
|
||||||
|
|
||||||
|
logger = _Logger() # pylint: disable=invalid-name
|
||||||
|
|
|
@ -4,7 +4,7 @@ Notification widgets.
|
||||||
import urwid
|
import urwid
|
||||||
|
|
||||||
|
|
||||||
class Notification(urwid.Columns):
|
class _Notification(urwid.Columns):
|
||||||
"""
|
"""
|
||||||
Single notification widget.
|
Single notification widget.
|
||||||
Can be updated or closed.
|
Can be updated or closed.
|
||||||
|
@ -16,7 +16,7 @@ class Notification(urwid.Columns):
|
||||||
self._id = notification_id
|
self._id = notification_id
|
||||||
self.text = urwid.Text('')
|
self.text = urwid.Text('')
|
||||||
self._set_text(message)
|
self._set_text(message)
|
||||||
super(Notification, self).__init__([
|
super(_Notification, self).__init__([
|
||||||
urwid.AttrWrap(
|
urwid.AttrWrap(
|
||||||
urwid.Columns([
|
urwid.Columns([
|
||||||
self.text,
|
self.text,
|
||||||
|
@ -41,7 +41,7 @@ class Notification(urwid.Columns):
|
||||||
message = '\n'.join([
|
message = '\n'.join([
|
||||||
message[0]
|
message[0]
|
||||||
] + [' {}'.format(line) for line in message[1:]])
|
] + [' {}'.format(line) for line in message[1:]])
|
||||||
self.text.set_text(Notification.TEMPLATE.format(message))
|
self.text.set_text(_Notification.TEMPLATE.format(message))
|
||||||
|
|
||||||
def update(self, message):
|
def update(self, message):
|
||||||
"""
|
"""
|
||||||
|
@ -50,7 +50,7 @@ class Notification(urwid.Columns):
|
||||||
self._set_text(message)
|
self._set_text(message)
|
||||||
if not self.is_alive:
|
if not self.is_alive:
|
||||||
self.area.append_notification(self)
|
self.area.append_notification(self)
|
||||||
self.area.__class__.app.redraw()
|
self.area.app.redraw()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_alive(self):
|
def is_alive(self):
|
||||||
|
@ -70,73 +70,36 @@ class Notification(urwid.Columns):
|
||||||
if notification is self:
|
if notification is self:
|
||||||
self.area.contents.remove((notification, props))
|
self.area.contents.remove((notification, props))
|
||||||
|
|
||||||
if self.area.__class__.app is not None:
|
if self.area.app is not None:
|
||||||
self.area.__class__.app.redraw()
|
self.area.app.redraw()
|
||||||
|
|
||||||
|
|
||||||
class NotificationArea(urwid.Pile):
|
class _NotificationArea(urwid.Pile):
|
||||||
"""
|
"""
|
||||||
Notification area widget.
|
Notification area widget.
|
||||||
"""
|
"""
|
||||||
instance = None
|
|
||||||
app = None
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
assert self.__class__.instance is None, 'Can be created only once!'
|
self.app = None
|
||||||
self.last_id = 0
|
self.last_id = 0
|
||||||
self.notifications = {}
|
self.notifications = {}
|
||||||
super(NotificationArea, self).__init__([])
|
super(_NotificationArea, self).__init__([])
|
||||||
|
|
||||||
@classmethod
|
def set_app(self, app):
|
||||||
def get(cls):
|
|
||||||
"""
|
|
||||||
Create new :class:`.NotificationArea` instance or return existing one.
|
|
||||||
"""
|
|
||||||
if cls.instance is None:
|
|
||||||
cls.instance = NotificationArea()
|
|
||||||
|
|
||||||
return cls.instance
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def set_app(cls, app):
|
|
||||||
"""
|
"""
|
||||||
Set app instance.
|
Set app instance.
|
||||||
|
|
||||||
Required for proper screen redraws when
|
Required for proper screen redraws when
|
||||||
new notifications are created asynchronously.
|
new notifications are created asynchronously.
|
||||||
"""
|
"""
|
||||||
cls.app = app
|
self.app = app
|
||||||
|
|
||||||
@classmethod
|
def notify(self, message):
|
||||||
def notify(cls, message):
|
|
||||||
"""
|
|
||||||
Create new notification with message.
|
|
||||||
This is a class method.
|
|
||||||
"""
|
|
||||||
return cls.get().do_notify(message)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def close_all(cls):
|
|
||||||
"""
|
|
||||||
Close all notfiications.
|
|
||||||
This is a class method.
|
|
||||||
"""
|
|
||||||
cls.get().do_close_all()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def close_newest(cls):
|
|
||||||
"""
|
|
||||||
Close newest notification.
|
|
||||||
This is a class method.
|
|
||||||
"""
|
|
||||||
cls.get().do_close_newest()
|
|
||||||
|
|
||||||
def do_notify(self, message):
|
|
||||||
"""
|
"""
|
||||||
Create new notification with message.
|
Create new notification with message.
|
||||||
"""
|
"""
|
||||||
self.last_id += 1
|
self.last_id += 1
|
||||||
notification = Notification(self, self.last_id, message)
|
notification = _Notification(self, self.last_id, message)
|
||||||
self.append_notification(notification)
|
self.append_notification(notification)
|
||||||
return notification
|
return notification
|
||||||
|
|
||||||
|
@ -150,20 +113,23 @@ class NotificationArea(urwid.Pile):
|
||||||
('weight', 1)
|
('weight', 1)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if self.__class__.app is not None:
|
if self.app is not None:
|
||||||
self.__class__.app.redraw()
|
self.app.redraw()
|
||||||
|
|
||||||
def do_close_all(self):
|
def close_all(self):
|
||||||
"""
|
"""
|
||||||
Close all notifications.
|
Close all notifications.
|
||||||
"""
|
"""
|
||||||
while self.contents:
|
while self.contents:
|
||||||
self.contents[0][0].close()
|
self.contents[0][0].close()
|
||||||
|
|
||||||
def do_close_newest(self):
|
def close_newest(self):
|
||||||
"""
|
"""
|
||||||
Close newest notification
|
Close newest notification
|
||||||
"""
|
"""
|
||||||
if not self.contents:
|
if not self.contents:
|
||||||
return
|
return
|
||||||
self.contents[-1][0].close()
|
self.contents[-1][0].close()
|
||||||
|
|
||||||
|
|
||||||
|
notification_area = _NotificationArea() # pylint: disable=invalid-name
|
||||||
|
|
|
@ -4,9 +4,9 @@ Debug page.
|
||||||
import urwid
|
import urwid
|
||||||
|
|
||||||
from clay.pages.page import AbstractPage
|
from clay.pages.page import AbstractPage
|
||||||
from clay.log import Logger
|
from clay.log import logger
|
||||||
from clay.clipboard import copy
|
from clay.clipboard import copy
|
||||||
from clay.gp import GP
|
from clay.gp import gp
|
||||||
|
|
||||||
|
|
||||||
class DebugItem(urwid.AttrMap):
|
class DebugItem(urwid.AttrMap):
|
||||||
|
@ -49,9 +49,9 @@ class DebugPage(urwid.Pile, AbstractPage):
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
self.app = app
|
self.app = app
|
||||||
self.walker = urwid.SimpleListWalker([])
|
self.walker = urwid.SimpleListWalker([])
|
||||||
for log_record in Logger.get().get_logs():
|
for log_record in logger.get_logs():
|
||||||
self._append_log(log_record)
|
self._append_log(log_record)
|
||||||
Logger.get().on_log_event += self._append_log
|
logger.on_log_event += self._append_log
|
||||||
self.listbox = urwid.ListBox(self.walker)
|
self.listbox = urwid.ListBox(self.walker)
|
||||||
|
|
||||||
self.debug_data = urwid.Text('')
|
self.debug_data = urwid.Text('')
|
||||||
|
@ -64,7 +64,7 @@ class DebugPage(urwid.Pile, AbstractPage):
|
||||||
self.listbox
|
self.listbox
|
||||||
])
|
])
|
||||||
|
|
||||||
GP.get().auth_state_changed += self.update
|
gp.auth_state_changed += self.update
|
||||||
|
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
|
@ -72,12 +72,11 @@ class DebugPage(urwid.Pile, AbstractPage):
|
||||||
"""
|
"""
|
||||||
Update this widget.
|
Update this widget.
|
||||||
"""
|
"""
|
||||||
gpclient = GP.get()
|
|
||||||
self.debug_data.set_text(
|
self.debug_data.set_text(
|
||||||
'- Is authenticated: {}\n'
|
'- Is authenticated: {}\n'
|
||||||
'- Is subscribed: {}'.format(
|
'- Is subscribed: {}'.format(
|
||||||
gpclient.is_authenticated,
|
gp.is_authenticated,
|
||||||
gpclient.is_subscribed if gpclient.is_authenticated else None
|
gp.is_subscribed if gp.is_authenticated else None
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,9 @@ Library page.
|
||||||
"""
|
"""
|
||||||
import urwid
|
import urwid
|
||||||
|
|
||||||
from clay.gp import GP
|
from clay.gp import gp
|
||||||
from clay.songlist import SongListBox
|
from clay.songlist import SongListBox
|
||||||
from clay.notifications import NotificationArea
|
from clay.notifications import notification_area
|
||||||
from clay.pages.page import AbstractPage
|
from clay.pages.page import AbstractPage
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,8 +28,8 @@ class MyLibraryPage(urwid.Columns, AbstractPage):
|
||||||
self.songlist = SongListBox(app)
|
self.songlist = SongListBox(app)
|
||||||
self.notification = None
|
self.notification = None
|
||||||
|
|
||||||
GP.get().auth_state_changed += self.get_all_songs
|
gp.auth_state_changed += self.get_all_songs
|
||||||
GP.get().caches_invalidated += self.get_all_songs
|
gp.caches_invalidated += self.get_all_songs
|
||||||
|
|
||||||
super(MyLibraryPage, self).__init__([
|
super(MyLibraryPage, self).__init__([
|
||||||
self.songlist
|
self.songlist
|
||||||
|
@ -41,7 +41,7 @@ class MyLibraryPage(urwid.Columns, AbstractPage):
|
||||||
Populate song list.
|
Populate song list.
|
||||||
"""
|
"""
|
||||||
if error:
|
if error:
|
||||||
NotificationArea.notify('Failed to load my library: {}'.format(str(error)))
|
notification_area.notify('Failed to load my library: {}'.format(str(error)))
|
||||||
return
|
return
|
||||||
# self.notification.close()
|
# self.notification.close()
|
||||||
self.songlist.populate(tracks)
|
self.songlist.populate(tracks)
|
||||||
|
@ -51,12 +51,11 @@ class MyLibraryPage(urwid.Columns, AbstractPage):
|
||||||
"""
|
"""
|
||||||
Called when auth state changes or GP caches are invalidated.
|
Called when auth state changes or GP caches are invalidated.
|
||||||
"""
|
"""
|
||||||
if GP.get().is_authenticated:
|
if gp.is_authenticated:
|
||||||
self.songlist.set_placeholder(u'\n \uf01e Loading song list...')
|
self.songlist.set_placeholder(u'\n \uf01e Loading song list...')
|
||||||
|
|
||||||
GP.get().get_all_tracks_async(callback=self.on_get_all_songs)
|
gp.get_all_tracks_async(callback=self.on_get_all_songs)
|
||||||
self.app.redraw()
|
self.app.redraw()
|
||||||
# self.notification = NotificationArea.notify('Loading library...')
|
|
||||||
|
|
||||||
def activate(self):
|
def activate(self):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -3,9 +3,9 @@ Components for "My playlists" page.
|
||||||
"""
|
"""
|
||||||
import urwid
|
import urwid
|
||||||
|
|
||||||
from clay.gp import GP
|
from clay.gp import gp
|
||||||
from clay.songlist import SongListBox
|
from clay.songlist import SongListBox
|
||||||
from clay.notifications import NotificationArea
|
from clay.notifications import notification_area
|
||||||
from clay.pages.page import AbstractPage
|
from clay.pages.page import AbstractPage
|
||||||
|
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ class MyPlaylistListBox(urwid.ListBox):
|
||||||
])
|
])
|
||||||
self.notification = None
|
self.notification = None
|
||||||
|
|
||||||
GP.get().auth_state_changed += self.auth_state_changed
|
gp.auth_state_changed += self.auth_state_changed
|
||||||
|
|
||||||
super(MyPlaylistListBox, self).__init__(self.walker)
|
super(MyPlaylistListBox, self).__init__(self.walker)
|
||||||
|
|
||||||
|
@ -73,9 +73,7 @@ class MyPlaylistListBox(urwid.ListBox):
|
||||||
urwid.Text(u'\n \uf01e Loading playlists...', align='center')
|
urwid.Text(u'\n \uf01e Loading playlists...', align='center')
|
||||||
]
|
]
|
||||||
|
|
||||||
GP.get().get_all_user_playlist_contents_async(callback=self.on_get_playlists)
|
gp.get_all_user_playlist_contents_async(callback=self.on_get_playlists)
|
||||||
|
|
||||||
# self.notification = NotificationArea.notify('Loading playlists...')
|
|
||||||
|
|
||||||
def on_get_playlists(self, playlists, error):
|
def on_get_playlists(self, playlists, error):
|
||||||
"""
|
"""
|
||||||
|
@ -83,7 +81,7 @@ class MyPlaylistListBox(urwid.ListBox):
|
||||||
Populates list of playlists.
|
Populates list of playlists.
|
||||||
"""
|
"""
|
||||||
if error:
|
if error:
|
||||||
NotificationArea.notify('Failed to get playlists: {}'.format(str(error)))
|
notification_area.notify('Failed to get playlists: {}'.format(str(error)))
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
for playlist in playlists:
|
for playlist in playlists:
|
||||||
|
|
|
@ -4,7 +4,7 @@ Components for "Queue" page.
|
||||||
import urwid
|
import urwid
|
||||||
|
|
||||||
from clay.songlist import SongListBox
|
from clay.songlist import SongListBox
|
||||||
from clay.player import Player
|
from clay.player import player
|
||||||
from clay.pages.page import AbstractPage
|
from clay.pages.page import AbstractPage
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,7 +24,6 @@ class QueuePage(urwid.Columns, AbstractPage):
|
||||||
self.app = app
|
self.app = app
|
||||||
self.songlist = SongListBox(app)
|
self.songlist = SongListBox(app)
|
||||||
|
|
||||||
player = Player.get()
|
|
||||||
self.songlist.populate(player.get_queue_tracks())
|
self.songlist.populate(player.get_queue_tracks())
|
||||||
player.queue_changed += self.queue_changed
|
player.queue_changed += self.queue_changed
|
||||||
player.track_appended += self.track_appended
|
player.track_appended += self.track_appended
|
||||||
|
@ -39,7 +38,7 @@ class QueuePage(urwid.Columns, AbstractPage):
|
||||||
Called when player queue is changed.
|
Called when player queue is changed.
|
||||||
Updates this queue widget.
|
Updates this queue widget.
|
||||||
"""
|
"""
|
||||||
self.songlist.populate(Player.get().get_queue_tracks())
|
self.songlist.populate(player.get_queue_tracks())
|
||||||
|
|
||||||
def track_appended(self, track):
|
def track_appended(self, track):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -3,9 +3,9 @@ Components for search page.
|
||||||
"""
|
"""
|
||||||
import urwid
|
import urwid
|
||||||
|
|
||||||
from clay.gp import GP
|
from clay.gp import gp
|
||||||
from clay.songlist import SongListBox
|
from clay.songlist import SongListBox
|
||||||
from clay.notifications import NotificationArea
|
from clay.notifications import notification_area
|
||||||
from clay.pages.page import AbstractPage
|
from clay.pages.page import AbstractPage
|
||||||
|
|
||||||
|
|
||||||
|
@ -76,14 +76,14 @@ class SearchPage(urwid.Pile, AbstractPage):
|
||||||
self.songlist.set_placeholder(u' \U0001F50D Searching for "{}"...'.format(
|
self.songlist.set_placeholder(u' \U0001F50D Searching for "{}"...'.format(
|
||||||
query
|
query
|
||||||
))
|
))
|
||||||
GP.get().search_async(query, callback=self.search_finished)
|
gp.search_async(query, callback=self.search_finished)
|
||||||
|
|
||||||
def search_finished(self, results, error):
|
def search_finished(self, results, error):
|
||||||
"""
|
"""
|
||||||
Populate song list with search results.
|
Populate song list with search results.
|
||||||
"""
|
"""
|
||||||
if error:
|
if error:
|
||||||
NotificationArea.notify('Failed to search: {}'.format(str(error)))
|
notification_area.notify('Failed to search: {}'.format(str(error)))
|
||||||
else:
|
else:
|
||||||
self.songlist.populate(results.get_tracks())
|
self.songlist.populate(results.get_tracks())
|
||||||
self.app.redraw()
|
self.app.redraw()
|
||||||
|
|
|
@ -4,8 +4,8 @@ Components for "Settings" page.
|
||||||
import urwid
|
import urwid
|
||||||
|
|
||||||
from clay.pages.page import AbstractPage
|
from clay.pages.page import AbstractPage
|
||||||
from clay.settings import Settings
|
from clay.settings import settings
|
||||||
from clay.player import Player
|
from clay.player import player
|
||||||
|
|
||||||
|
|
||||||
class Slider(urwid.Widget):
|
class Slider(urwid.Widget):
|
||||||
|
@ -31,9 +31,9 @@ class Slider(urwid.Widget):
|
||||||
freq = int(freq)
|
freq = int(freq)
|
||||||
self.freq = freq
|
self.freq = freq
|
||||||
if freq >= 1000:
|
if freq >= 1000:
|
||||||
self.freq_str = str(freq // 1000) + ' KHz'
|
self.freq_str = str(freq // 1000) + '\nKHz'
|
||||||
else:
|
else:
|
||||||
self.freq_str = str(freq) + ' Hz'
|
self.freq_str = str(freq) + '\nHz'
|
||||||
self.value = 0
|
self.value = 0
|
||||||
self.slider_height = 5
|
self.slider_height = 5
|
||||||
self.max_value = 20
|
self.max_value = 20
|
||||||
|
@ -43,7 +43,7 @@ class Slider(urwid.Widget):
|
||||||
"""
|
"""
|
||||||
Return count of rows required to render this widget.
|
Return count of rows required to render this widget.
|
||||||
"""
|
"""
|
||||||
return self.slider_height + 2
|
return self.slider_height + 3
|
||||||
|
|
||||||
def render(self, size, focus=None):
|
def render(self, size, focus=None):
|
||||||
"""
|
"""
|
||||||
|
@ -95,7 +95,7 @@ class Slider(urwid.Widget):
|
||||||
"""
|
"""
|
||||||
Update player equalizer & toggle redraw.
|
Update player equalizer & toggle redraw.
|
||||||
"""
|
"""
|
||||||
Player.get().set_equalizer_value(self.index, self.value)
|
player.set_equalizer_value(self.index, self.value)
|
||||||
self._invalidate()
|
self._invalidate()
|
||||||
|
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ class Equalizer(urwid.Columns):
|
||||||
self.bands = [
|
self.bands = [
|
||||||
Slider(index, freq)
|
Slider(index, freq)
|
||||||
for index, freq
|
for index, freq
|
||||||
in enumerate(Player.get().get_equalizer_freqs())
|
in enumerate(player.get_equalizer_freqs())
|
||||||
]
|
]
|
||||||
super(Equalizer, self).__init__(
|
super(Equalizer, self).__init__(
|
||||||
self.bands
|
self.bands
|
||||||
|
@ -128,20 +128,18 @@ class SettingsPage(urwid.Columns, AbstractPage):
|
||||||
|
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
self.app = app
|
self.app = app
|
||||||
config = Settings.get_config('play_settings')
|
|
||||||
|
|
||||||
self.username = urwid.Edit(
|
self.username = urwid.Edit(
|
||||||
edit_text=config.get('username', '')
|
edit_text=settings.get('username', default='')
|
||||||
)
|
)
|
||||||
self.password = urwid.Edit(
|
self.password = urwid.Edit(
|
||||||
mask='*', edit_text=config.get('password', '')
|
mask='*', edit_text=settings.get('password', default='')
|
||||||
)
|
)
|
||||||
self.device_id = urwid.Edit(
|
self.device_id = urwid.Edit(
|
||||||
edit_text=config.get('device_id', '')
|
edit_text=settings.get('device_id', default='')
|
||||||
)
|
)
|
||||||
self.download_tracks = urwid.CheckBox(
|
self.download_tracks = urwid.CheckBox(
|
||||||
'Download tracks before playback',
|
'Download tracks before playback',
|
||||||
state=config.get('download_tracks', False)
|
state=settings.get('download_tracks', default=False)
|
||||||
)
|
)
|
||||||
self.equalizer = Equalizer()
|
self.equalizer = Equalizer()
|
||||||
super(SettingsPage, self).__init__([urwid.ListBox(urwid.SimpleListWalker([
|
super(SettingsPage, self).__init__([urwid.ListBox(urwid.SimpleListWalker([
|
||||||
|
@ -169,12 +167,12 @@ class SettingsPage(urwid.Columns, AbstractPage):
|
||||||
"""
|
"""
|
||||||
Called when "Save" button is pressed.
|
Called when "Save" button is pressed.
|
||||||
"""
|
"""
|
||||||
Settings.set_config(dict(
|
with settings.edit() as config:
|
||||||
username=self.username.edit_text,
|
config['username'] = self.username.edit_text
|
||||||
password=self.password.edit_text,
|
config['password'] = self.password.edit_text
|
||||||
device_id=self.device_id.edit_text,
|
config['device_id'] = self.device_id.edit_text
|
||||||
download_tracks=self.download_tracks.state
|
config['download_tracks'] = self.download_tracks.state
|
||||||
))
|
|
||||||
self.app.set_page('MyLibraryPage')
|
self.app.set_page('MyLibraryPage')
|
||||||
self.app.log_in()
|
self.app.log_in()
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ PlayBar widget.
|
||||||
# pylint: disable=too-many-instance-attributes
|
# pylint: disable=too-many-instance-attributes
|
||||||
import urwid
|
import urwid
|
||||||
|
|
||||||
from clay.player import Player
|
from clay.player import player
|
||||||
from clay import meta
|
from clay import meta
|
||||||
|
|
||||||
|
|
||||||
|
@ -98,7 +98,6 @@ class PlayBar(urwid.Pile):
|
||||||
])
|
])
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
player = Player.get()
|
|
||||||
player.media_position_changed += self.update
|
player.media_position_changed += self.update
|
||||||
player.media_state_changed += self.update
|
player.media_state_changed += self.update
|
||||||
player.track_changed += self.update
|
player.track_changed += self.update
|
||||||
|
@ -115,7 +114,6 @@ class PlayBar(urwid.Pile):
|
||||||
"""
|
"""
|
||||||
Return the style for current playback state.
|
Return the style for current playback state.
|
||||||
"""
|
"""
|
||||||
player = Player.get()
|
|
||||||
if player.is_loading or player.is_playing:
|
if player.is_loading or player.is_playing:
|
||||||
return 'title-playing'
|
return 'title-playing'
|
||||||
return 'title-idle'
|
return 'title-idle'
|
||||||
|
@ -124,7 +122,6 @@ class PlayBar(urwid.Pile):
|
||||||
"""
|
"""
|
||||||
Return text for display in this bar.
|
Return text for display in this bar.
|
||||||
"""
|
"""
|
||||||
player = Player.get()
|
|
||||||
track = player.get_current_track()
|
track = player.get_current_track()
|
||||||
if track is None:
|
if track is None:
|
||||||
return u'{} {}'.format(
|
return u'{} {}'.format(
|
||||||
|
@ -153,7 +150,6 @@ class PlayBar(urwid.Pile):
|
||||||
Called when something unrelated to completion value changes,
|
Called when something unrelated to completion value changes,
|
||||||
e.g. current track or playback flags.
|
e.g. current track or playback flags.
|
||||||
"""
|
"""
|
||||||
player = Player.get()
|
|
||||||
self.text.set_text(self.get_text())
|
self.text.set_text(self.get_text())
|
||||||
self.progressbar.set_progress(player.get_play_progress())
|
self.progressbar.set_progress(player.get_play_progress())
|
||||||
self.progressbar.set_done_style(
|
self.progressbar.set_done_style(
|
||||||
|
|
|
@ -14,11 +14,12 @@ except ImportError: # Python 2.x
|
||||||
|
|
||||||
from clay import vlc, meta
|
from clay import vlc, meta
|
||||||
from clay.eventhook import EventHook
|
from clay.eventhook import EventHook
|
||||||
from clay.notifications import NotificationArea
|
from clay.notifications import notification_area
|
||||||
from clay.settings import Settings
|
from clay.settings import settings
|
||||||
from clay.log import Logger
|
from clay.log import logger
|
||||||
|
|
||||||
class Queue(object):
|
|
||||||
|
class _Queue(object):
|
||||||
"""
|
"""
|
||||||
Model that represents player queue (local playlist),
|
Model that represents player queue (local playlist),
|
||||||
i.e. list of tracks to be played.
|
i.e. list of tracks to be played.
|
||||||
|
@ -111,15 +112,13 @@ class Queue(object):
|
||||||
return self.tracks
|
return self.tracks
|
||||||
|
|
||||||
|
|
||||||
class Player(object):
|
class _Player(object):
|
||||||
"""
|
"""
|
||||||
Interface to libVLC. Uses Queue as a playback plan.
|
Interface to libVLC. Uses Queue as a playback plan.
|
||||||
Emits various events if playback state, tracks or play flags change.
|
Emits various events if playback state, tracks or play flags change.
|
||||||
|
|
||||||
Singleton.
|
Singleton.
|
||||||
"""
|
"""
|
||||||
instance = None
|
|
||||||
|
|
||||||
media_position_changed = EventHook()
|
media_position_changed = EventHook()
|
||||||
media_state_changed = EventHook()
|
media_state_changed = EventHook()
|
||||||
track_changed = EventHook()
|
track_changed = EventHook()
|
||||||
|
@ -129,9 +128,6 @@ class Player(object):
|
||||||
track_removed = EventHook()
|
track_removed = EventHook()
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
assert self.__class__.instance is None, 'Can be created only once!'
|
|
||||||
self.logger = Logger.get()
|
|
||||||
|
|
||||||
self.instance = vlc.Instance()
|
self.instance = vlc.Instance()
|
||||||
self.instance.set_user_agent(
|
self.instance.set_user_agent(
|
||||||
meta.APP_NAME,
|
meta.APP_NAME,
|
||||||
|
@ -163,26 +159,15 @@ class Player(object):
|
||||||
|
|
||||||
self._create_station_notification = None
|
self._create_station_notification = None
|
||||||
self._is_loading = False
|
self._is_loading = False
|
||||||
self.queue = Queue()
|
self.queue = _Queue()
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get(cls):
|
|
||||||
"""
|
|
||||||
Create new :class:`.Player` instance or return existing one.
|
|
||||||
"""
|
|
||||||
if cls.instance is None:
|
|
||||||
cls.instance = Player()
|
|
||||||
|
|
||||||
return cls.instance
|
|
||||||
|
|
||||||
def enable_xorg_bindings(self):
|
def enable_xorg_bindings(self):
|
||||||
"""Enable the global X bindings using keybinder"""
|
"""Enable the global X bindings using keybinder"""
|
||||||
if os.environ.get("DISPLAY") is None:
|
if os.environ.get("DISPLAY") is None:
|
||||||
self.logger.debug("X11 isn't running so we can't load the global keybinds")
|
logger.debug("X11 isn't running so we can't load the global keybinds")
|
||||||
return
|
return
|
||||||
|
|
||||||
from clay.hotkeys import HotkeyManager
|
from clay.hotkeys import hotkey_manager
|
||||||
hotkey_manager = HotkeyManager.get()
|
|
||||||
hotkey_manager.play_pause += self.play_pause
|
hotkey_manager.play_pause += self.play_pause
|
||||||
hotkey_manager.next += self.next
|
hotkey_manager.next += self.next
|
||||||
hotkey_manager.prev += lambda: self.seek_absolute(0)
|
hotkey_manager.prev += lambda: self.seek_absolute(0)
|
||||||
|
@ -247,7 +232,7 @@ class Player(object):
|
||||||
Load queue & start playback.
|
Load queue & start playback.
|
||||||
Fires :attr:`.queue_changed` event.
|
Fires :attr:`.queue_changed` event.
|
||||||
|
|
||||||
See :meth:`.Queue.load`.
|
See :meth:`._Queue.load`.
|
||||||
"""
|
"""
|
||||||
self.queue.load(data, current_index)
|
self.queue.load(data, current_index)
|
||||||
self.queue_changed.fire()
|
self.queue_changed.fire()
|
||||||
|
@ -258,7 +243,7 @@ class Player(object):
|
||||||
Append track to queue.
|
Append track to queue.
|
||||||
Fires :attr:`.track_appended` event.
|
Fires :attr:`.track_appended` event.
|
||||||
|
|
||||||
See :meth:`.Queue.append`
|
See :meth:`._Queue.append`
|
||||||
"""
|
"""
|
||||||
self.queue.append(track)
|
self.queue.append(track)
|
||||||
self.track_appended.fire(track)
|
self.track_appended.fire(track)
|
||||||
|
@ -269,7 +254,7 @@ class Player(object):
|
||||||
Remove track from queue.
|
Remove track from queue.
|
||||||
Fires :attr:`.track_removed` event.
|
Fires :attr:`.track_removed` event.
|
||||||
|
|
||||||
See :meth:`.Queue.remove`
|
See :meth:`._Queue.remove`
|
||||||
"""
|
"""
|
||||||
self.queue.remove(track)
|
self.queue.remove(track)
|
||||||
self.track_removed.fire(track)
|
self.track_removed.fire(track)
|
||||||
|
@ -279,7 +264,7 @@ class Player(object):
|
||||||
Request creation of new station from some track.
|
Request creation of new station from some track.
|
||||||
Runs in background.
|
Runs in background.
|
||||||
"""
|
"""
|
||||||
self._create_station_notification = NotificationArea.notify('Creating station...')
|
self._create_station_notification = notification_area.notify('Creating station...')
|
||||||
track.create_station_async(callback=self._create_station_from_track_ready)
|
track.create_station_async(callback=self._create_station_from_track_ready)
|
||||||
|
|
||||||
def _create_station_from_track_ready(self, station, error):
|
def _create_station_from_track_ready(self, station, error):
|
||||||
|
@ -330,7 +315,7 @@ class Player(object):
|
||||||
|
|
||||||
def get_queue_tracks(self):
|
def get_queue_tracks(self):
|
||||||
"""
|
"""
|
||||||
Return :attr:`.Queue.get_tracks`
|
Return :attr:`._Queue.get_tracks`
|
||||||
"""
|
"""
|
||||||
return self.queue.get_tracks()
|
return self.queue.get_tracks()
|
||||||
|
|
||||||
|
@ -346,29 +331,30 @@ class Player(object):
|
||||||
self.broadcast_state()
|
self.broadcast_state()
|
||||||
self.track_changed.fire(track)
|
self.track_changed.fire(track)
|
||||||
|
|
||||||
if Settings.get_config("play_settings").get('download_tracks', False):
|
if settings.get('download_tracks', default=False) or settings.get_is_file_cached(track.filename):
|
||||||
path = Settings.get_cached_file_path(track.store_id + '.mp3')
|
path = settings.get_cached_file_path(track.filename)
|
||||||
|
|
||||||
if path is None:
|
if path is None:
|
||||||
self.logger.debug('Track %s not in cache, downloading...', track.store_id)
|
logger.debug('Track %s not in cache, downloading...', track.store_id)
|
||||||
track.get_url(callback=self._download_track)
|
track.get_url(callback=self._download_track)
|
||||||
else:
|
else:
|
||||||
self.logger.debug('Track %s in cache, playing', track.store_id)
|
logger.debug('Track %s in cache, playing', track.store_id)
|
||||||
self._play_ready(path, None, track)
|
self._play_ready(path, None, track)
|
||||||
else:
|
else:
|
||||||
self.logger.debug('Starting to stream %s', track.store_id)
|
logger.debug('Starting to stream %s', track.store_id)
|
||||||
track.get_url(callback=self._play_ready)
|
track.get_url(callback=self._play_ready)
|
||||||
|
|
||||||
def _download_track(self, url, error, track):
|
def _download_track(self, url, error, track):
|
||||||
if error:
|
if error:
|
||||||
NotificationArea.notify('Failed to request media URL: {}'.format(str(error)))
|
notification_area.notify('Failed to request media URL: {}'.format(str(error)))
|
||||||
self.logger.error(
|
logger.error(
|
||||||
'Failed to request media URL for track %s: %s',
|
'Failed to request media URL for track %s: %s',
|
||||||
track.original_data,
|
track.original_data,
|
||||||
str(error)
|
str(error)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
response = urlopen(url)
|
response = urlopen(url)
|
||||||
path = Settings.save_file_to_cache(track.store_id + '.mp3', response.read())
|
path = settings.save_file_to_cache(track.filename, response.read())
|
||||||
self._play_ready(path, None, track)
|
self._play_ready(path, None, track)
|
||||||
|
|
||||||
def _play_ready(self, url, error, track):
|
def _play_ready(self, url, error, track):
|
||||||
|
@ -378,8 +364,8 @@ class Player(object):
|
||||||
"""
|
"""
|
||||||
self._is_loading = False
|
self._is_loading = False
|
||||||
if error:
|
if error:
|
||||||
NotificationArea.notify('Failed to request media URL: {}'.format(str(error)))
|
notification_area.notify('Failed to request media URL: {}'.format(str(error)))
|
||||||
self.logger.error(
|
logger.error(
|
||||||
'Failed to request media URL for track %s: %s',
|
'Failed to request media URL for track %s: %s',
|
||||||
track.original_data,
|
track.original_data,
|
||||||
str(error)
|
str(error)
|
||||||
|
@ -435,7 +421,7 @@ class Player(object):
|
||||||
def next(self, force=False):
|
def next(self, force=False):
|
||||||
"""
|
"""
|
||||||
Advance to next track in queue.
|
Advance to next track in queue.
|
||||||
See :meth:`.Queue.next`.
|
See :meth:`._Queue.next`.
|
||||||
"""
|
"""
|
||||||
self.queue.next(force)
|
self.queue.next(force)
|
||||||
self._play()
|
self._play()
|
||||||
|
@ -443,7 +429,7 @@ class Player(object):
|
||||||
def get_current_track(self):
|
def get_current_track(self):
|
||||||
"""
|
"""
|
||||||
Return currently played track.
|
Return currently played track.
|
||||||
See :meth:`.Queue.get_current_track`.
|
See :meth:`._Queue.get_current_track`.
|
||||||
"""
|
"""
|
||||||
return self.queue.get_current_track()
|
return self.queue.get_current_track()
|
||||||
|
|
||||||
|
@ -508,3 +494,6 @@ class Player(object):
|
||||||
index
|
index
|
||||||
) == 0
|
) == 0
|
||||||
self.media_player.set_equalizer(self.equalizer)
|
self.media_player.set_equalizer(self.equalizer)
|
||||||
|
|
||||||
|
|
||||||
|
player = _Player() # pylint: disable=invalid-name
|
||||||
|
|
155
clay/settings.py
155
clay/settings.py
|
@ -1,77 +1,158 @@
|
||||||
"""
|
"""
|
||||||
Application settings manager.
|
Application settings manager.
|
||||||
"""
|
"""
|
||||||
|
from threading import Lock
|
||||||
import os
|
import os
|
||||||
|
import copy
|
||||||
import errno
|
import errno
|
||||||
import yaml
|
import yaml
|
||||||
import appdirs
|
import appdirs
|
||||||
|
|
||||||
|
|
||||||
# Rewrite this so it keeps the settings in memory and writes on exit
|
class _SettingsEditor(dict):
|
||||||
# It is sort of silly to use so much IO for simple tasks
|
"""
|
||||||
class Settings(object):
|
Thread-safe settings editor context manager.
|
||||||
|
|
||||||
|
For example see :py:meth:`~._Settings.edit`.
|
||||||
|
"""
|
||||||
|
_lock = Lock()
|
||||||
|
|
||||||
|
def __init__(self, original_config, commit_callback):
|
||||||
|
super(_SettingsEditor, self).__init__()
|
||||||
|
_SettingsEditor._lock.acquire()
|
||||||
|
self._commit_callback = commit_callback
|
||||||
|
self.update(copy.deepcopy(original_config))
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, exc_tb):
|
||||||
|
_SettingsEditor._lock.release()
|
||||||
|
if exc_tb is None:
|
||||||
|
self._commit_callback(self)
|
||||||
|
else:
|
||||||
|
# TODO: Handle this
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class _Settings(object):
|
||||||
"""
|
"""
|
||||||
Settings management class.
|
Settings management class.
|
||||||
"""
|
"""
|
||||||
@classmethod
|
def __init__(self):
|
||||||
def get_config_filename(cls):
|
self._config = {}
|
||||||
"""
|
self._cached_files = set()
|
||||||
Returns full path to config file and will create it if it doesn't
|
|
||||||
already exist.
|
|
||||||
"""
|
|
||||||
filedir = appdirs.user_config_dir('clay', 'Clay')
|
|
||||||
path = os.path.join(filedir, 'config.yaml')
|
|
||||||
|
|
||||||
if os.path.exists(path):
|
self._config_dir = None
|
||||||
return path
|
self._config_file_path = None
|
||||||
|
self._cache_dir = None
|
||||||
|
|
||||||
|
self._ensure_directories()
|
||||||
|
self._load_config()
|
||||||
|
self._load_cache()
|
||||||
|
|
||||||
|
def _ensure_directories(self):
|
||||||
|
"""
|
||||||
|
Create config dir, config file & cache dir if they do not exist yet.
|
||||||
|
"""
|
||||||
|
self._config_dir = appdirs.user_config_dir('clay', 'Clay')
|
||||||
|
self._config_file_path = os.path.join(self._config_dir, 'config.yaml')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.makedirs(filedir)
|
os.makedirs(self._config_dir)
|
||||||
except OSError as error:
|
except OSError as error:
|
||||||
if error.errno != errno.EEXIST:
|
if error.errno != errno.EEXIST:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
with open(path, 'w') as settings:
|
self._cache_dir = appdirs.user_cache_dir('clay', 'Clay')
|
||||||
settings.write('{}')
|
try:
|
||||||
|
os.makedirs(self._cache_dir)
|
||||||
|
except OSError as error:
|
||||||
|
if error.errno != errno.EEXIST:
|
||||||
|
raise
|
||||||
|
|
||||||
return path
|
if not os.path.exists(self._config_file_path):
|
||||||
|
with open(self._config_file_path, 'w') as settings_file:
|
||||||
|
settings_file.write('{}')
|
||||||
|
|
||||||
@classmethod
|
def _load_config(self):
|
||||||
def get_config(cls, section):
|
|
||||||
"""
|
"""
|
||||||
Read config dictionary.
|
Read config from file.
|
||||||
"""
|
"""
|
||||||
with open(Settings.get_config_filename(), 'r') as settings:
|
with open(self._config_file_path, 'r') as settings_file:
|
||||||
return yaml.load(settings.read()).get(section)
|
self._config = yaml.load(settings_file.read())
|
||||||
|
|
||||||
@classmethod
|
def _load_cache(self):
|
||||||
def set_config(cls, new_config):
|
|
||||||
"""
|
"""
|
||||||
Write config dictionary.
|
Load cached files.
|
||||||
"""
|
"""
|
||||||
config = Settings.get_config('play_settings')
|
self._cached_files = set(os.listdir(self._cache_dir))
|
||||||
config.update(new_config)
|
|
||||||
with open(Settings.get_config_filename(), 'w') as settings:
|
|
||||||
settings.write(yaml.dump(config, default_flow_style=False))
|
|
||||||
|
|
||||||
@classmethod
|
def _commit_edits(self, config):
|
||||||
def get_cached_file_path(cls, filename):
|
"""
|
||||||
|
Write config to file.
|
||||||
|
|
||||||
|
This method is supposed to be called only
|
||||||
|
from :py:meth:`~._SettingsEditor.__exit__`.
|
||||||
|
>>>>>>> master
|
||||||
|
"""
|
||||||
|
self._config.update(config)
|
||||||
|
with open(self._config_file_path, 'w') as settings_file:
|
||||||
|
settings_file.write(yaml.dump(self._config, default_flow_style=False))
|
||||||
|
|
||||||
|
def get(self, key, section="play_settings", default=None):
|
||||||
|
if section not in self._config:
|
||||||
|
return default
|
||||||
|
|
||||||
|
return self._config[section].get(key, default)
|
||||||
|
|
||||||
|
def get_section(self, key, default=None):
|
||||||
|
"""
|
||||||
|
Return config value.
|
||||||
|
"""
|
||||||
|
return self._config.get(key, default)
|
||||||
|
|
||||||
|
def edit(self):
|
||||||
|
"""
|
||||||
|
Return :py:class:`._SettingsEditor` context manager to edit config.
|
||||||
|
|
||||||
|
Settings are saved to file once the returned context manager exists.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from clay.settings import settings
|
||||||
|
|
||||||
|
with settings.edit() as config:
|
||||||
|
config['foo']['bar'] = 'baz'
|
||||||
|
"""
|
||||||
|
return _SettingsEditor(self._config, self._commit_edits)
|
||||||
|
|
||||||
|
def get_cached_file_path(self, filename):
|
||||||
"""
|
"""
|
||||||
Get full path to cached file.
|
Get full path to cached file.
|
||||||
"""
|
"""
|
||||||
cache_dir = appdirs.user_cache_dir('clay', 'Clay')
|
path = os.path.join(self._cache_dir, filename)
|
||||||
path = os.path.join(cache_dir, filename)
|
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
return path
|
return path
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
def get_is_file_cached(self, filename):
|
||||||
def save_file_to_cache(cls, filename, content):
|
"""
|
||||||
|
Return ``True`` if *filename* is present in cache.
|
||||||
|
"""
|
||||||
|
return filename in self._cached_files
|
||||||
|
|
||||||
|
def save_file_to_cache(self, filename, content):
|
||||||
"""
|
"""
|
||||||
Save content into file in cache.
|
Save content into file in cache.
|
||||||
"""
|
"""
|
||||||
cache_dir = appdirs.user_cache_dir('clay', 'Clay')
|
path = os.path.join(self._cache_dir, filename)
|
||||||
path = os.path.join(cache_dir, filename)
|
|
||||||
with open(path, 'wb') as cachefile:
|
with open(path, 'wb') as cachefile:
|
||||||
cachefile.write(content)
|
cachefile.write(content)
|
||||||
|
self._cached_files.add(filename)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
settings = _Settings() # pylint: disable=invalid-name
|
||||||
|
|
158
clay/songlist.py
158
clay/songlist.py
|
@ -12,10 +12,11 @@ except ImportError:
|
||||||
# Python 2.3
|
# Python 2.3
|
||||||
from string import letters as ascii_letters
|
from string import letters as ascii_letters
|
||||||
import urwid
|
import urwid
|
||||||
from clay.notifications import NotificationArea
|
from clay.notifications import notification_area
|
||||||
from clay.player import Player
|
from clay.player import player
|
||||||
from clay.gp import GP
|
from clay.gp import gp
|
||||||
from clay.clipboard import copy
|
from clay.clipboard import copy
|
||||||
|
from clay.settings import settings
|
||||||
|
|
||||||
|
|
||||||
class SongListItem(urwid.Pile):
|
class SongListItem(urwid.Pile):
|
||||||
|
@ -35,6 +36,19 @@ class SongListItem(urwid.Pile):
|
||||||
STATE_PLAYING = 2
|
STATE_PLAYING = 2
|
||||||
STATE_PAUSED = 3
|
STATE_PAUSED = 3
|
||||||
|
|
||||||
|
LINE1_ATTRS = {
|
||||||
|
STATE_IDLE: ('line1', 'line1_focus'),
|
||||||
|
STATE_LOADING: ('line1_active', 'line1_active_focus'),
|
||||||
|
STATE_PLAYING: ('line1_active', 'line1_active_focus'),
|
||||||
|
STATE_PAUSED: ('line1_active', 'line1_active_focus')
|
||||||
|
}
|
||||||
|
LINE2_ATTRS = {
|
||||||
|
STATE_IDLE: ('line2', 'line2_focus'),
|
||||||
|
STATE_LOADING: ('line2', 'line2_focus'),
|
||||||
|
STATE_PLAYING: ('line2', 'line2_focus'),
|
||||||
|
STATE_PAUSED: ('line2', 'line2_focus')
|
||||||
|
}
|
||||||
|
|
||||||
STATE_ICONS = {
|
STATE_ICONS = {
|
||||||
0: ' ',
|
0: ' ',
|
||||||
1: u'\u2505',
|
1: u'\u2505',
|
||||||
|
@ -46,8 +60,14 @@ class SongListItem(urwid.Pile):
|
||||||
self.track = track
|
self.track = track
|
||||||
self.index = 0
|
self.index = 0
|
||||||
self.state = SongListItem.STATE_IDLE
|
self.state = SongListItem.STATE_IDLE
|
||||||
self.line1 = urwid.SelectableIcon('', cursor_position=1000)
|
self.line1_left = urwid.SelectableIcon('', cursor_position=1000)
|
||||||
self.line1.set_layout('left', 'clip', None)
|
self.line1_left.set_layout('left', 'clip', None)
|
||||||
|
self.line1_right = urwid.Text('x')
|
||||||
|
self.line1 = urwid.Columns([
|
||||||
|
self.line1_left,
|
||||||
|
('pack', self.line1_right),
|
||||||
|
('pack', urwid.Text(' '))
|
||||||
|
])
|
||||||
self.line2 = urwid.Text('', wrap='clip')
|
self.line2 = urwid.Text('', wrap='clip')
|
||||||
|
|
||||||
self.line1_wrap = urwid.AttrWrap(self.line1, 'line1')
|
self.line1_wrap = urwid.AttrWrap(self.line1, 'line1')
|
||||||
|
@ -90,13 +110,7 @@ class SongListItem(urwid.Pile):
|
||||||
"""
|
"""
|
||||||
Update text of this item from the attached track.
|
Update text of this item from the attached track.
|
||||||
"""
|
"""
|
||||||
if self.state == SongListItem.STATE_IDLE:
|
self.line1_left.set_text(
|
||||||
title_attr = 'line1_focus' if self.is_focused else 'line1'
|
|
||||||
else:
|
|
||||||
title_attr = 'line1_active_focus' if self.is_focused else 'line1_active'
|
|
||||||
artist_attr = 'line2_focus' if self.is_focused else 'line2'
|
|
||||||
|
|
||||||
self.line1.set_text(
|
|
||||||
u'{index:3d} {icon} {title} [{minutes:02d}:{seconds:02d}]'.format(
|
u'{index:3d} {icon} {title} [{minutes:02d}:{seconds:02d}]'.format(
|
||||||
index=self.index + 1,
|
index=self.index + 1,
|
||||||
icon=self.get_state_icon(self.state),
|
icon=self.get_state_icon(self.state),
|
||||||
|
@ -105,11 +119,15 @@ class SongListItem(urwid.Pile):
|
||||||
seconds=(self.track.duration // 1000) % 60
|
seconds=(self.track.duration // 1000) % 60
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if settings.get_is_file_cached(self.track.filename):
|
||||||
|
self.line1_right.set_text(u' \u25bc Cached')
|
||||||
|
else:
|
||||||
|
self.line1_right.set_text(u'')
|
||||||
self.line2.set_text(
|
self.line2.set_text(
|
||||||
u' {} \u2015 {}'.format(self.track.artist, self.track.album_name)
|
u' {} \u2015 {}'.format(self.track.artist, self.track.album_name)
|
||||||
)
|
)
|
||||||
self.line1_wrap.set_attr(title_attr)
|
self.line1_wrap.set_attr(SongListItem.LINE1_ATTRS[self.state][self.is_focused])
|
||||||
self.line2_wrap.set_attr(artist_attr)
|
self.line2_wrap.set_attr(SongListItem.LINE2_ATTRS[self.state][self.is_focused])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def full_title(self):
|
def full_title(self):
|
||||||
|
@ -198,70 +216,42 @@ class SongListBoxPopup(urwid.LineBox):
|
||||||
'panel_divider'
|
'panel_divider'
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
options.append(urwid.AttrWrap(
|
options.append(self._create_divider())
|
||||||
urwid.Divider(u'\u2500'),
|
if not gp.get_track_by_id(songitem.track.id):
|
||||||
'panel_divider',
|
options.append(self._create_button('Add to library', self.add_to_my_library))
|
||||||
'panel_divider_focus'
|
|
||||||
))
|
|
||||||
if not GP.get().get_track_by_id(songitem.track.id):
|
|
||||||
options.append(urwid.AttrWrap(
|
|
||||||
urwid.Button('Add to my library', on_press=self.add_to_my_library),
|
|
||||||
'panel',
|
|
||||||
'panel_focus'
|
|
||||||
))
|
|
||||||
else:
|
else:
|
||||||
options.append(urwid.AttrWrap(
|
options.append(self._create_button('Remove from library', self.remove_from_my_library))
|
||||||
urwid.Button('Remove from my library', on_press=self.remove_from_my_library),
|
options.append(self._create_divider())
|
||||||
'panel',
|
options.append(self._create_button('Create station', self.create_station))
|
||||||
'panel_focus'
|
options.append(self._create_divider())
|
||||||
))
|
if self.songitem.track in player.get_queue_tracks():
|
||||||
options.append(urwid.AttrWrap(
|
options.append(self._create_button('Remove from queue', self.remove_from_queue))
|
||||||
urwid.Divider(u'\u2500'),
|
|
||||||
'panel_divider',
|
|
||||||
'panel_divider_focus'
|
|
||||||
))
|
|
||||||
options.append(urwid.AttrWrap(
|
|
||||||
urwid.Button('Create station', on_press=self.create_station),
|
|
||||||
'panel',
|
|
||||||
'panel_focus'
|
|
||||||
))
|
|
||||||
options.append(urwid.AttrWrap(
|
|
||||||
urwid.Divider(u'\u2500'),
|
|
||||||
'panel_divider',
|
|
||||||
'panel_divider_focus'
|
|
||||||
))
|
|
||||||
if self.songitem.track in Player.get().get_queue_tracks():
|
|
||||||
options.append(urwid.AttrWrap(
|
|
||||||
urwid.Button('Remove from queue', on_press=self.remove_from_queue),
|
|
||||||
'panel',
|
|
||||||
'panel_focus'
|
|
||||||
))
|
|
||||||
else:
|
else:
|
||||||
options.append(urwid.AttrWrap(
|
options.append(self._create_button('Append to queue', self.append_to_queue))
|
||||||
urwid.Button('Append to queue', on_press=self.append_to_queue),
|
|
||||||
'panel',
|
|
||||||
'panel_focus'
|
|
||||||
))
|
|
||||||
options.append(urwid.AttrWrap(
|
|
||||||
urwid.Divider(u'\u2500'),
|
|
||||||
'panel_divider',
|
|
||||||
'panel_divider_focus'
|
|
||||||
))
|
|
||||||
if self.songitem.track.cached_url is not None:
|
if self.songitem.track.cached_url is not None:
|
||||||
options.append(urwid.AttrWrap(
|
options.append(self._create_button('Copy URL to clipboard', self.copy_url))
|
||||||
urwid.Button('Copy URL to clipboard', on_press=self.copy_url),
|
options.append(self._create_button('Close', self.close))
|
||||||
'panel',
|
|
||||||
'panel_focus'
|
|
||||||
))
|
|
||||||
options.append(urwid.AttrWrap(
|
|
||||||
urwid.Button('Close', on_press=self.close),
|
|
||||||
'panel',
|
|
||||||
'panel_focus'
|
|
||||||
))
|
|
||||||
super(SongListBoxPopup, self).__init__(
|
super(SongListBoxPopup, self).__init__(
|
||||||
urwid.Pile(options)
|
urwid.Pile(options)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _create_divider(self):
|
||||||
|
"""
|
||||||
|
Return a divider widget.
|
||||||
|
"""
|
||||||
|
return urwid.AttrWrap(
|
||||||
|
urwid.Divider(u'\u2500'),
|
||||||
|
'panel_divider',
|
||||||
|
'panel_divider_focus'
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_button(self, title, on_press):
|
||||||
|
return urwid.AttrWrap(
|
||||||
|
urwid.Button(title, on_press=on_press),
|
||||||
|
'panel',
|
||||||
|
'panel_focus'
|
||||||
|
)
|
||||||
|
|
||||||
def add_to_my_library(self, _):
|
def add_to_my_library(self, _):
|
||||||
"""
|
"""
|
||||||
Add related track to my library.
|
Add related track to my library.
|
||||||
|
@ -271,11 +261,11 @@ class SongListBoxPopup(urwid.LineBox):
|
||||||
Show notification with song addition result.
|
Show notification with song addition result.
|
||||||
"""
|
"""
|
||||||
if error or not result:
|
if error or not result:
|
||||||
NotificationArea.notify('Error while adding track to my library: {}'.format(
|
notification_area.notify('Error while adding track to my library: {}'.format(
|
||||||
str(error) if error else 'reason is unknown :('
|
str(error) if error else 'reason is unknown :('
|
||||||
))
|
))
|
||||||
else:
|
else:
|
||||||
NotificationArea.notify('Track added to library!')
|
notification_area.notify('Track added to library!')
|
||||||
self.songitem.track.add_to_my_library_async(callback=on_add_to_my_library)
|
self.songitem.track.add_to_my_library_async(callback=on_add_to_my_library)
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
@ -288,11 +278,11 @@ class SongListBoxPopup(urwid.LineBox):
|
||||||
Show notification with song removal result.
|
Show notification with song removal result.
|
||||||
"""
|
"""
|
||||||
if error or not result:
|
if error or not result:
|
||||||
NotificationArea.notify('Error while removing track from my library: {}'.format(
|
notification_area.notify('Error while removing track from my library: {}'.format(
|
||||||
str(error) if error else 'reason is unknown :('
|
str(error) if error else 'reason is unknown :('
|
||||||
))
|
))
|
||||||
else:
|
else:
|
||||||
NotificationArea.notify('Track removed from library!')
|
notification_area.notify('Track removed from library!')
|
||||||
self.songitem.track.remove_from_my_library_async(callback=on_remove_from_my_library)
|
self.songitem.track.remove_from_my_library_async(callback=on_remove_from_my_library)
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
@ -300,21 +290,21 @@ class SongListBoxPopup(urwid.LineBox):
|
||||||
"""
|
"""
|
||||||
Appends related track to queue.
|
Appends related track to queue.
|
||||||
"""
|
"""
|
||||||
Player.get().append_to_queue(self.songitem.track)
|
player.append_to_queue(self.songitem.track)
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def remove_from_queue(self, _):
|
def remove_from_queue(self, _):
|
||||||
"""
|
"""
|
||||||
Removes related track from queue.
|
Removes related track from queue.
|
||||||
"""
|
"""
|
||||||
Player.get().remove_from_queue(self.songitem.track)
|
player.remove_from_queue(self.songitem.track)
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def create_station(self, _):
|
def create_station(self, _):
|
||||||
"""
|
"""
|
||||||
Create a station from this track.
|
Create a station from this track.
|
||||||
"""
|
"""
|
||||||
Player.get().create_station_from_track(self.songitem.track)
|
player.create_station_from_track(self.songitem.track)
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def copy_url(self, _):
|
def copy_url(self, _):
|
||||||
|
@ -344,7 +334,6 @@ class SongListBox(urwid.Frame):
|
||||||
self.tracks = []
|
self.tracks = []
|
||||||
self.walker = urwid.SimpleFocusListWalker([])
|
self.walker = urwid.SimpleFocusListWalker([])
|
||||||
|
|
||||||
player = Player.get()
|
|
||||||
player.track_changed += self.track_changed
|
player.track_changed += self.track_changed
|
||||||
player.media_state_changed += self.media_state_changed
|
player.media_state_changed += self.media_state_changed
|
||||||
|
|
||||||
|
@ -432,7 +421,7 @@ class SongListBox(urwid.Frame):
|
||||||
"""
|
"""
|
||||||
Convert list of track data items into list of :class:`.SongListItem` instances.
|
Convert list of track data items into list of :class:`.SongListItem` instances.
|
||||||
"""
|
"""
|
||||||
current_track = Player.get().get_current_track()
|
current_track = player.get_current_track()
|
||||||
items = []
|
items = []
|
||||||
current_index = None
|
current_index = None
|
||||||
for index, track in enumerate(tracks):
|
for index, track in enumerate(tracks):
|
||||||
|
@ -465,7 +454,6 @@ class SongListBox(urwid.Frame):
|
||||||
Toggles track playback state or loads entire playlist
|
Toggles track playback state or loads entire playlist
|
||||||
that contains current track into player queue.
|
that contains current track into player queue.
|
||||||
"""
|
"""
|
||||||
player = Player.get()
|
|
||||||
if songitem.is_currently_played:
|
if songitem.is_currently_played:
|
||||||
player.play_pause()
|
player.play_pause()
|
||||||
else:
|
else:
|
||||||
|
@ -477,7 +465,7 @@ class SongListBox(urwid.Frame):
|
||||||
Called when specific item emits *append-requested* item.
|
Called when specific item emits *append-requested* item.
|
||||||
Appends track to player queue.
|
Appends track to player queue.
|
||||||
"""
|
"""
|
||||||
Player.get().append_to_queue(songitem.track)
|
player.append_to_queue(songitem.track)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def item_unappend_requested(songitem):
|
def item_unappend_requested(songitem):
|
||||||
|
@ -485,7 +473,7 @@ class SongListBox(urwid.Frame):
|
||||||
Called when specific item emits *remove-requested* item.
|
Called when specific item emits *remove-requested* item.
|
||||||
Removes track from player queue.
|
Removes track from player queue.
|
||||||
"""
|
"""
|
||||||
Player.get().remove_from_queue(songitem.track)
|
player.remove_from_queue(songitem.track)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def item_station_requested(songitem):
|
def item_station_requested(songitem):
|
||||||
|
@ -493,7 +481,7 @@ class SongListBox(urwid.Frame):
|
||||||
Called when specific item emits *station-requested* item.
|
Called when specific item emits *station-requested* item.
|
||||||
Requests new station creation.
|
Requests new station creation.
|
||||||
"""
|
"""
|
||||||
Player.get().create_station_from_track(songitem.track)
|
player.create_station_from_track(songitem.track)
|
||||||
|
|
||||||
def context_menu_requested(self, songitem):
|
def context_menu_requested(self, songitem):
|
||||||
"""
|
"""
|
||||||
|
@ -540,7 +528,7 @@ class SongListBox(urwid.Frame):
|
||||||
Called when player media state changes.
|
Called when player media state changes.
|
||||||
Updates corresponding song item state (if found in this song list).
|
Updates corresponding song item state (if found in this song list).
|
||||||
"""
|
"""
|
||||||
current_track = Player.get().get_current_track()
|
current_track = player.get_current_track()
|
||||||
if current_track is None:
|
if current_track is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
3
tox.ini
3
tox.ini
|
@ -12,5 +12,4 @@ deps =
|
||||||
gmusicapi
|
gmusicapi
|
||||||
pylint
|
pylint
|
||||||
commands =
|
commands =
|
||||||
pylint clay
|
make check
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue