Repository: postlund/pyatv
Pull request #50 (2017-02-16 19:37:01)

Better output with "playing" command in atvremote

coveralls: 
[![Coverage Status](https://coveralls.io/builds/10190727/badge)](https://coveralls.io/builds/10190727)

Coverage decreased (-1.8%) to 93.333% when pulling **96e37d17d8acdffa9a8820d99d4196488ba6d442 on playing_output** into **640a96dbd9ee07619f2297f0f7107551811635f5 on master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/10190727/badge)](https://coveralls.io/builds/10190727)

Coverage decreased (-1.8%) to 93.333% when pulling **96e37d17d8acdffa9a8820d99d4196488ba6d442 on playing_output** into **640a96dbd9ee07619f2297f0f7107551811635f5 on master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/10190727/badge)](https://coveralls.io/builds/10190727)

Coverage decreased (-1.8%) to 93.333% when pulling **96e37d17d8acdffa9a8820d99d4196488ba6d442 on playing_output** into **640a96dbd9ee07619f2297f0f7107551811635f5 on master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/10190727/badge)](https://coveralls.io/builds/10190727)

Coverage decreased (-1.8%) to 93.333% when pulling **96e37d17d8acdffa9a8820d99d4196488ba6d442 on playing_output** into **640a96dbd9ee07619f2297f0f7107551811635f5 on master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/10198676/badge)](https://coveralls.io/builds/10198676)

Coverage increased (+0.9%) to 96.061% when pulling **494d09d8ad70ba856d8b720fd79e2c51c19c385c on playing_output** into **640a96dbd9ee07619f2297f0f7107551811635f5 on master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/10198742/badge)](https://coveralls.io/builds/10198742)

Coverage increased (+0.9%) to 96.061% when pulling **abfd7569917dea0ed5f45dffd5104ebf6fae4fe1 on playing_output** into **640a96dbd9ee07619f2297f0f7107551811635f5 on master**.


Patch:
diff --git a/pyatv/convert.py b/pyatv/convert.py
index 7a20fc66..98c7e77e 100644
--- a/pyatv/convert.py
+++ b/pyatv/convert.py
@@ -17,6 +17,20 @@ def media_kind(kind):
     raise exceptions.UnknownMediaKind('Unknown media kind: ' + str(kind))
 
 
+def media_type_str(mediatype):
+    """Convert internal API media type to string."""
+    if mediatype == const.MEDIA_TYPE_UNKNOWN:
+        return 'Unknown'
+    elif mediatype == const.MEDIA_TYPE_VIDEO:
+        return 'Video'
+    elif mediatype == const.MEDIA_TYPE_MUSIC:
+        return 'Music'
+    elif mediatype == const.MEDIA_TYPE_TV:
+        return 'TV'
+    else:
+        return 'Unsupported'
+
+
 def playstate(state):
     """Convert iTunes playstate to API representation."""
     if state is None:
@@ -35,6 +49,25 @@ def playstate(state):
     raise exceptions.UnknownPlayState('Unknown playstate: ' + str(state))
 
 
+# pylint: disable=too-many-return-statements
+def playstate_str(state):
+    """Convert internal API playstate to string."""
+    if state == const.PLAY_STATE_NO_MEDIA:
+        return 'No media'
+    elif state == const.PLAY_STATE_LOADING:
+        return 'Loading'
+    elif state == const.PLAY_STATE_PAUSED:
+        return 'Paused'
+    elif state == const.PLAY_STATE_PLAYING:
+        return 'Playing'
+    elif state == const.PLAY_STATE_FAST_FORWARD:
+        return 'Fast forward'
+    elif state == const.PLAY_STATE_FAST_BACKWARD:
+        return 'Fast backward'
+    else:
+        return 'Unsupported'
+
+
 def ms_to_s(time):
     """Convert time in ms to seconds."""
     if time is None:
diff --git a/pyatv/interface.py b/pyatv/interface.py
index fd5f4b9c..e99bb82c 100644
--- a/pyatv/interface.py
+++ b/pyatv/interface.py
@@ -5,7 +5,7 @@ import inspect
 
 from abc import (ABCMeta, abstractmethod, abstractproperty)
 
-from pyatv import exceptions
+from pyatv import (convert, exceptions)
 
 
 # TODO: make these methods more pretty and safe
@@ -119,6 +119,35 @@ class Playing(object):
 
     __metaclass__ = ABCMeta
 
+    def __str__(self):
+        """Convert this playing object to a readable string."""
+        output = []
+        output.append('Media type: {0}'.format(
+            convert.media_type_str(self.media_type)))
+        output.append('Play state: {0}'.format(
+            convert.playstate_str(self.play_state)))
+
+        if self.title is not None:
+            output.append('     Title: {0}'.format(self.title))
+
+        if self.artist is not None:
+            output.append('    Artist: {0}'.format(self.artist))
+
+        if self.album is not None:
+            output.append('     Album: {0}'.format(self.album))
+
+        position = self.position
+        total_time = self.total_time
+        if position is not None and total_time is not None and total_time != 0:
+            output.append('  Position: {0}/{1}s ({2:.1%})'.format(
+                position, total_time, float(position)/float(total_time)))
+        elif position is not None and position != 0:
+            output.append('  Position: {0}s'.format(position))
+        elif total_time is not None and position != 0:
+            output.append('Total time: {0}s'.format(total_time))
+
+        return '\n'.join(output)
+
     @abstractproperty
     def media_type(self):
         """What type of media is currently playing, e.g. video, music."""
diff --git a/pyatv/internal/apple_tv.py b/pyatv/internal/apple_tv.py
index 6c732efb..80054d97 100644
--- a/pyatv/internal/apple_tv.py
+++ b/pyatv/internal/apple_tv.py
@@ -8,7 +8,7 @@ generations of devices. Everything is however left here for now.
 import logging
 import asyncio
 
-from pyatv import (const, exceptions, dmap, tags, interface, convert)
+from pyatv import (const, exceptions, dmap, tags, convert)
 from pyatv.interface import (AppleTV, RemoteControl, Metadata, Playing)
 
 
@@ -169,13 +169,6 @@ class PlayingInternal(Playing):
         self.apple_tv = apple_tv
         self.playstatus = playstatus
 
-    def __str__(self):
-        """Convert this playing object to a readable string."""
-        def _to_str(value_mapping):
-            return '{}: {}'.format(value_mapping, getattr(self, value_mapping))
-        values = interface.retrieve_commands(self).keys()
-        return '\n'.join(map(_to_str, sorted(values)))
-
     @property
     def media_type(self):
         """What type of media is currently playing, e.g. video, music."""


Test patch:
diff --git a/tests/test_convert.py b/tests/test_convert.py
index 65c44fce..99d8bdd3 100644
--- a/tests/test_convert.py
+++ b/tests/test_convert.py
@@ -83,6 +83,18 @@ class ConvertTest(unittest.TestCase):
         with self.assertRaises(exceptions.UnknownMediaKind):
             convert.media_kind(99999)
 
+    def test_media_type_to_string(self):
+        self.assertEqual('Unknown',
+                         convert.media_type_str(const.MEDIA_TYPE_UNKNOWN))
+        self.assertEqual('Video',
+                         convert.media_type_str(const.MEDIA_TYPE_VIDEO))
+        self.assertEqual('Music',
+                         convert.media_type_str(const.MEDIA_TYPE_MUSIC))
+        self.assertEqual('TV', convert.media_type_str(const.MEDIA_TYPE_TV))
+
+    def test_unknown_media_type_to_str_throws(self):
+        self.assertEqual('Unsupported', convert.media_type_str(999))
+
     # PLAYSTATE TESTS
 
     def test_play_state_no_media(self):
@@ -93,7 +105,7 @@ class ConvertTest(unittest.TestCase):
         self.assertEqual(const.PLAY_STATE_NO_MEDIA,
                          convert.playstate(None))
 
-    def test_regular_play_states(self):
+    def test_regular_playstates(self):
         self.assertEqual(const.PLAY_STATE_LOADING,
                          convert.playstate(PLAY_STATE_LOADING))
         self.assertEqual(const.PLAY_STATE_PAUSED,
@@ -109,6 +121,23 @@ class ConvertTest(unittest.TestCase):
         with self.assertRaises(exceptions.UnknownPlayState):
             convert.playstate(99999)
 
+    def test_playstate_str(self):
+        self.assertEqual('No media',
+                         convert.playstate_str(const.PLAY_STATE_NO_MEDIA))
+        self.assertEqual('Loading',
+                         convert.playstate_str(const.PLAY_STATE_LOADING))
+        self.assertEqual('Paused',
+                         convert.playstate_str(const.PLAY_STATE_PAUSED))
+        self.assertEqual('Playing',
+                         convert.playstate_str(const.PLAY_STATE_PLAYING))
+        self.assertEqual('Fast forward',
+                         convert.playstate_str(const.PLAY_STATE_FAST_FORWARD))
+        self.assertEqual('Fast backward',
+                         convert.playstate_str(const.PLAY_STATE_FAST_BACKWARD))
+
+    def test_unsupported_playstate_str(self):
+        self.assertEqual('Unsupported', convert.playstate_str(999))
+
     # TIME TESTS
 
     def test_no_time_returns_zero(self):
diff --git a/tests/test_interface.py b/tests/test_interface.py
index d6c1971c..1dbda21b 100644
--- a/tests/test_interface.py
+++ b/tests/test_interface.py
@@ -2,7 +2,7 @@
 
 import unittest
 
-from pyatv import interface
+from pyatv import (const, convert, interface)
 
 
 class TestClass:
@@ -39,12 +39,63 @@ class TestClass:
         pass
 
 
+class PlayingDummy(interface.Playing):
+
+    def __init__(self, media_type=const.MEDIA_TYPE_VIDEO,
+                 play_state=const.PLAY_STATE_PLAYING, title=None, artist=None,
+                 album=None, total_time=None, position=None):
+        self._media_type = media_type
+        self._play_state = play_state
+        self._title = title
+        self._artist = artist
+        self._album = album
+        self._total_time = total_time
+        self._position = position
+
+    @property
+    def media_type(self):
+        """What type of media is currently playing, e.g. video, music."""
+        return self._media_type
+
+    @property
+    def play_state(self):
+        """Current play state, e.g. playing or paused."""
+        return self._play_state
+
+    @property
+    def title(self):
+        """Title of the current media, e.g. movie or song name."""
+        return self._title
+
+    @property
+    def artist(self):
+        """Artist of the currently playing song."""
+        return self._artist
+
+    @property
+    def album(self):
+        """Album of the currently playing song."""
+        return self._album
+
+    @property
+    def total_time(self):
+        """Total play time in seconds."""
+        return self._total_time
+
+    @property
+    def position(self):
+        """Current position in the playing media (seconds)."""
+        return self._position
+
+
 class InterfaceTest(unittest.TestCase):
 
     def setUp(self):
         self.obj = TestClass()
         self.methods = interface.retrieve_commands(self.obj)
 
+    # COMMANDS AND HELP TEXT
+
     def test_get_commands(self):
         self.assertEqual(5, len(self.methods))
         self.assertTrue('test_method' in self.methods)
@@ -68,3 +119,27 @@ class InterfaceTest(unittest.TestCase):
             'Type, e.g. a, b or c', self.methods['abbrev_help'])
         self.assertEqual(
             'Type, e.g. a, b or c', self.methods['abbrev_help_more_text'])
+
+    # PLAYING STR
+
+    def test_playing_media_type_and_playstate(self):
+        out = str(PlayingDummy())
+        self.assertIn(convert.media_type_str(const.MEDIA_TYPE_VIDEO), out)
+        self.assertIn(convert.playstate_str(const.PLAY_STATE_PLAYING), out)
+
+    def test_playing_title_artist_album(self):
+        out = str(PlayingDummy(
+            title='mytitle', artist='myartist', album='myalbum'))
+        self.assertIn('mytitle', out)
+        self.assertIn('myartist', out)
+        self.assertIn('myalbum', out)
+
+    def test_playing_only_position(self):
+        self.assertIn('1234', str(PlayingDummy(position=1234)))
+
+    def test_playing_only_total_time(self):
+        self.assertIn('5678', str(PlayingDummy(total_time=5678)))
+
+    def test_playing_both_position_and_total_time(self):
+        out = str(PlayingDummy(position=1234, total_time=5678))
+        self.assertIn('1234/5678', out)