Added debug page.

This commit is contained in:
Andrew Dunai 2018-01-31 23:56:51 +02:00
parent b48b147542
commit 801d691930
9 changed files with 267 additions and 31 deletions

View file

@ -160,12 +160,8 @@ I'll try to figure it out ASAP.
Most issues can be reproduced only with specific data coming from Google Play Music servers.
You can set `CLAY_DEBUG` environment variable to `1` before launching Clay player.
This will dump all Google API calls & responses to the `/tmp/clay-api-log.json` file.
You can attach this file when opening an issue here, but **keep in mind that
this file MAY contain some personal data, access tokens and/or Google account credentials
that may allow others to use your account**.
Use "debug" tab within app to select the error and hit "Enter" to copy it into clipboard.
This will help me to investigate this issue.
# Credits

View file

@ -13,6 +13,7 @@ import urwid
from clay.player import Player
from clay.playbar import PlayBar
from clay.pages.debug import DebugPage
from clay.pages.mylibrary import MyLibraryPage
from clay.pages.myplaylists import MyPlaylistsPage
from clay.pages.playerqueue import QueuePage
@ -120,6 +121,7 @@ class AppWidget(urwid.Frame):
def __init__(self):
self.pages = [
DebugPage(self),
MyLibraryPage(self),
MyPlaylistsPage(self),
QueuePage(self),

31
clay/clipboard.py Normal file
View file

@ -0,0 +1,31 @@
"""
Clipboard utils.
"""
from subprocess import Popen, PIPE
from clay.notifications import NotificationArea
COMMANDS = [
('xclip', '-selection', 'clipboard'),
('xsel', '-bi'),
]
def copy(text):
"""
Copy text to clipboard.
Return True on success.
"""
for cmd in COMMANDS:
proc = Popen(cmd, stdin=PIPE)
proc.communicate(text.encode('utf-8'))
if proc.returncode == 0:
return True
NotificationArea.notify(
'Failed to copy text to clipboard. '
'Please install "xclip" or "xsel".'
)
return False

View file

@ -6,15 +6,15 @@ Google Play Music integration via gmusicapi.
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-return-statements
# pylint: disable=protected-access
# pylint: disable=no-self-use
from __future__ import print_function
import os
import json
from threading import Thread, Lock
from uuid import UUID
from gmusicapi.clients import Mobileclient
from clay.eventhook import EventHook
from clay.log import Logger
def asynchronous(func):
@ -199,7 +199,12 @@ class Track(object):
album_name=track.album_name,
album_url=track.album_url
)
except Exception: # pylint: disable=bare-except
except Exception as error: # pylint: disable=bare-except
Logger.get().error(
'Failed to parse track data: %s, failing data: %s',
repr(error),
data
)
# TODO: Fix this.
# print('Failed to create track from data.')
# print('Failing payload was:')
@ -418,12 +423,12 @@ class GP(object):
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()
if self.is_debug:
self.mobile_client._make_call = self._make_call_proxy(self.mobile_client._make_call)
self.debug_file = open('/tmp/clay-api-log.json', 'w')
self._last_call_index = 0
# if self.is_debug:
# self.mobile_client._make_call = self._make_call_proxy(self.mobile_client._make_call)
# self.debug_file = open('/tmp/clay-api-log.json', 'w')
# self._last_call_index = 0
self.cached_tracks = None
self.cached_playlists = None
@ -449,15 +454,20 @@ class GP(object):
"""
Wrapper function.
"""
Logger.get().debug('GP::{}(*{}, **{})'.format(
protocol.__name__,
args,
kwargs
))
result = func(protocol, *args, **kwargs)
self._last_call_index += 1
call_index = self._last_call_index
self.debug_file.write(json.dumps([
call_index,
protocol.__name__, args, kwargs,
result
]) + '\n')
self.debug_file.flush()
# self._last_call_index += 1
# call_index = self._last_call_index
# self.debug_file.write(json.dumps([
# call_index,
# protocol.__name__, args, kwargs,
# result
# ]) + '\n')
# self.debug_file.flush()
return result
return _make_call

119
clay/log.py Normal file
View file

@ -0,0 +1,119 @@
"""
Logger implementation.
"""
# pylint: disable=too-few-public-methods
from threading import Lock
from datetime import datetime
from clay.eventhook import EventHook
class LoggerRecord(object):
"""
Represents a logger record.
"""
def __init__(self, verbosity, message, args):
self._timestamp = datetime.now()
self._verbosity = verbosity
self._message = message
self._args = args
@property
def formatted_timestamp(self):
"""
Return timestamp.
"""
return str(self._timestamp)
@property
def verbosity(self):
"""
Return verbosity.
"""
return self._verbosity
@property
def formatted_message(self):
"""
Return formatted message.
"""
return self._message % self._args
class Logger(object):
"""
Global logger.
Allows subscribing to log events.
"""
instance = None
def __init__(self):
assert self.__class__.instance is None, 'Can be created only once!'
self.logs = []
self.logfile = open('/tmp/clay.log', 'w')
self._lock = Lock()
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):
"""
Add log item.
"""
self._lock.acquire()
try:
logger_record = LoggerRecord(level, message, args)
self.logs.append(logger_record)
self.logfile.write('{} {:8} {}\n'.format(
logger_record.formatted_timestamp,
logger_record.verbosity,
logger_record.formatted_message
))
self.logfile.flush()
self.on_log_event.fire(logger_record)
finally:
self._lock.release()
def debug(self, message, *args):
"""
Add debug log item.
"""
self.log('DEBUG', message, *args)
def info(self, message, *args):
"""
Add info log item.
"""
self.log('INFO', message, *args)
def warn(self, message, *args):
"""
Add warning log item.
"""
self.log('WARNING', message, *args)
warning = warn
def error(self, message, *args):
"""
Add error log item.
"""
self.log('ERROR', message, *args)
def get_logs(self):
"""
Return all logs.
"""
return self.logs

View file

@ -7,7 +7,7 @@ except ImportError:
codename = None
APP_NAME = 'Clay Player'
VERSION = '0.5.5'
VERSION = '0.5.6'
if codename is not None:
VERSION_WITH_CODENAME = VERSION + '-' + codename(separator='-', id=VERSION)
else:

82
clay/pages/debug.py Normal file
View file

@ -0,0 +1,82 @@
"""
Debug page.
"""
import urwid
from clay.pages.page import AbstractPage
from clay.log import Logger
from clay.clipboard import copy
class DebugItem(urwid.AttrMap):
"""
Represents a single debug log item.
"""
def selectable(self):
return True
def __init__(self, log_record):
self.log_record = log_record
self.columns = urwid.Columns([
('pack', urwid.Text(self.log_record.verbosity.ljust(8))),
urwid.Text(
(
self.log_record.formatted_timestamp +
'\n' +
self.log_record.formatted_message
)
)
])
super(DebugItem, self).__init__(self.columns, 'panel', 'panel_focus')
def keypress(self, _, key):
"""
Handle heypress.
"""
if key == 'enter':
copy(self.log_record.formatted_message)
return None
return key
class DebugPage(urwid.Pile, AbstractPage):
"""
Represents debug page.
"""
def __init__(self, app):
self.app = app
self.walker = urwid.SimpleListWalker([])
for log_record in Logger.get().get_logs():
self._append_log(log_record)
Logger.get().on_log_event += self._append_log
self.listbox = urwid.ListBox(self.walker)
super(DebugPage, self).__init__([
('pack', urwid.Text('Hit "Enter" to copy selected message to clipboard.')),
self.listbox
])
def _append_log(self, log_record):
self.walker.insert(0, urwid.Divider(u'\u2500'))
self.walker.insert(0, DebugItem(log_record))
@property
def name(self):
"""
Return page name.
"""
return "Debug"
@property
def key(self):
"""
Return page key (``int``), used for hotkeys.
"""
return 0
def activate(self):
"""
Notify page that it is activated.
"""
pass

View file

@ -7,12 +7,14 @@ class AbstractPage(object):
"""
Represents app page.
"""
@property
def name(self):
"""
Return page name.
"""
raise NotImplementedError()
@property
def key(self):
"""
Return page key (``int``), used for hotkeys.

View file

@ -4,7 +4,6 @@ Components for song listing.
# pylint: disable=too-many-arguments
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-public-methods
import os
from string import digits
try:
# Python 3.x
@ -16,6 +15,7 @@ import urwid
from clay.notifications import NotificationArea
from clay.player import Player
from clay.gp import GP
from clay.clipboard import copy
class SongListItem(urwid.Pile):
@ -321,13 +321,7 @@ class SongListBoxPopup(urwid.LineBox):
"""
Copy URL to clipboard.
"""
code = os.system('echo "{}" | xclipa -selection clipboard'.format(
self.songitem.track.cached_url
))
if code != 0:
NotificationArea.notify(
'Failed to copy URL to clipboard, do you have "xclip" installed?'
)
copy(self.songitem.track.cached_url)
self.close()
def close(self, *_):