Repository: devsnd/tinytag

Issue:
Support BytesIo objects for tagging
It would be awesome to support BytesIO or any other IO types with TinyTag. This way it can be used in some scenarios where files are packaged and do not have a normal path.

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

coverage: 96.487% (-0.04%) from 96.529% when pulling **5484749b9c30480d90be9ad418e4c769717b0a68 on mathiascode:bytesio** into **337c044ce51f9e707b8cd31d02896c9041a98afa on devsnd:master**.


Patch:
diff --git a/tinytag/tinytag.py b/tinytag/tinytag.py
index a3f1305..595a101 100644
--- a/tinytag/tinytag.py
+++ b/tinytag/tinytag.py
@@ -170,40 +170,48 @@ class TinyTag(object):
                 return parser
 
     @classmethod
-    def get_parser_class(cls, filename, filehandle):
+    def get_parser_class(cls, filename=None, filehandle=None):
         if cls != TinyTag:  # if `get` is invoked on TinyTag, find parser by ext
             return cls  # otherwise use the class on which `get` was invoked
-        parser_class = cls._get_parser_for_filename(filename)
-        if parser_class is not None:
-            return parser_class
+        if filename:
+            parser_class = cls._get_parser_for_filename(filename)
+            if parser_class is not None:
+                return parser_class
         # try determining the file type by magic byte header
-        parser_class = cls._get_parser_for_file_handle(filehandle)
-        if parser_class is not None:
-            return parser_class
+        if filehandle:
+            parser_class = cls._get_parser_for_file_handle(filehandle)
+            if parser_class is not None:
+                return parser_class
         raise TinyTagException('No tag reader found to support filetype! ')
 
     @classmethod
-    def get(cls, filename, tags=True, duration=True, image=False, ignore_errors=False,
-            encoding=None):
-        try:  # cast pathlib.Path to str
-            import pathlib
-            if isinstance(filename, pathlib.Path):
-                filename = str(filename.absolute())
-        except ImportError:
-            pass
+    def get(cls, filename=None, tags=True, duration=True, image=False,
+            ignore_errors=False, encoding=None, file_obj=None):
+        should_open_file = (file_obj is None)
+        if should_open_file:
+            try:
+                file_obj = io.open(filename, 'rb')
+            except TypeError:
+                file_obj = io.open(str(filename.absolute()), 'rb')  # Python 3.4/3.5 pathlib support
+            filename = file_obj.name
         else:
-            filename = os.path.expanduser(filename)
-        size = os.path.getsize(filename)
-        if not size > 0:
-            return TinyTag(None, 0)
-        with io.open(filename, 'rb') as af:
-            parser_class = cls.get_parser_class(filename, af)
-            tag = parser_class(af, size, ignore_errors=ignore_errors)
+            file_obj = io.BufferedReader(file_obj)  # buffered reader to support peeking
+        try:
+            file_obj.seek(0, os.SEEK_END)
+            filesize = file_obj.tell()
+            file_obj.seek(0)
+            if filesize <= 0:
+                return TinyTag(None, filesize)
+            parser_class = cls.get_parser_class(filename, file_obj)
+            tag = parser_class(file_obj, filesize, ignore_errors=ignore_errors)
             tag._filename = filename
             tag._default_encoding = encoding
             tag.load(tags=tags, duration=duration, image=image)
             tag.extra = dict(tag.extra)  # turn default dict into dict so that it can throw KeyError
             return tag
+        finally:
+            if should_open_file:
+                file_obj.close()
 
     def __str__(self):
         return json.dumps(OrderedDict(sorted(self.as_dict().items())))


Test patch:
diff --git a/tinytag/tests/test_all.py b/tinytag/tests/test_all.py
index 0b70517..aa3a38e 100644
--- a/tinytag/tests/test_all.py
+++ b/tinytag/tests/test_all.py
@@ -510,6 +510,13 @@ def test_pathlib_compatibility():
     TinyTag.get(filename)
 
 
+def test_bytesio_compatibility():
+    testfile = next(iter(testfiles.keys()))
+    filename = os.path.join(testfolder, testfile)
+    with io.open(filename, 'rb') as file_handle:
+        TinyTag.get(file_obj=io.BytesIO(file_handle.read()))
+
+
 @pytest.mark.skipif(sys.platform == "win32", reason='Windows does not support binary paths')
 def test_binary_path_compatibility():
     binary_file_path = os.path.join(os.path.dirname(__file__).encode('utf-8'), b'\x01.mp3')