diff --git a/.travis.yml b/.travis.yml index 804449b..50064e7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,57 +1,10 @@ -dist: trusty language: python -sudo: false -env: - global: - # Undocumented feature of nose-show-skipped. - NOSE_SHOW_SKIPPED: 1 +services: + - docker -matrix: - include: - - python: 2.7.13 - env: {TOX_ENV: py27-cov, COVERAGE: 1} - - python: 2.7.13 - env: {TOX_ENV: py27-test} - - python: 3.4 - env: {TOX_ENV: py34-test} - - python: 3.5 - env: {TOX_ENV: py35-test} - - python: 3.6 - env: {TOX_ENV: py36-test} - - python: 3.7 - env: {TOX_ENV: py37-test} - dist: xenial - - python: 3.8-dev - env: {TOX_ENV: py38-test} - dist: xenial - - python: pypy - env: {TOX_ENV: pypy-test} - - python: 2.7 - env: {TOX_ENV: py27-flake8} - - python: 3.5 - env: {TOX_ENV: py35-flake8} - -# Non-Python dependencies. -addons: - apt: - packages: - - bash-completion - -# To install dependencies, tell tox to do everything but actually running the -# test. -install: - - travis_retry pip install 'tox<=3.8.1' - - travis_retry tox -e $TOX_ENV --notest - -script: - - tox -e $TOX_ENV - -# Report coverage to codecov.io. before_install: - - "[ ! -z $COVERAGE ] && travis_retry pip install codecov || true" -after_success: - - "[ ! -z $COVERAGE ] && codecov || true" + - make build -cache: - pip: true +script: + - make tox \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 0db5f07..1b10ac8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,7 +19,9 @@ RUN apt-get update \ && apt-get clean COPY requirements.txt . +COPY requirements-dev.txt . RUN pip install -r requirements.txt +RUN pip install -r requirements-dev.txt COPY . . RUN pip install . diff --git a/Makefile b/Makefile index 0386658..5634844 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,27 @@ tox: test: $(DOCKER_CMD) python -m unittest discover . +.PHONY: testpopm +testpopm: + $(DOCKER_CMD) python -m unittest test.test_mediafile.MP3Test + .PHONY: lint lint: $(DOCKER_CMD) flake8 + +.PHONY: ipython +ipython: + $(DOCKER_CMD) ipython + +.PHONY: virtualenv +virtualenv: + virtualenv --python=/usr/bin/python3.7 venv + . ./venv/bin/activate && \ + pip install -r requirements.txt && \ + pip install -r requirements-dev.txt && \ + pip install . + + +.PHONY: testpy +testpy: + $(DOCKER_CMD) python _test.py diff --git a/README.rst b/README.rst index 543de4e..f9e87c4 100644 --- a/README.rst +++ b/README.rst @@ -1,8 +1,8 @@ MediaFile: read and write audio files' tags in Python ===================================================== -.. image:: https://travis-ci.org/ifischer/mediafile.svg?branch=master - :target: https://travis-ci.org/ifischer/mediafile +.. image:: https://travis-ci.com/ifischer/mediafile.svg?branch=develop + :target: https://travis-ci.com/ifischer/mediafile .. image:: http://img.shields.io/pypi/v/mediafile.svg :target: https://pypi.python.org/pypi/mediafile diff --git a/_test.py b/_test.py new file mode 100644 index 0000000..2fe5fda --- /dev/null +++ b/_test.py @@ -0,0 +1,65 @@ +import os +from mediafile import * +from mutagen.id3 import ID3, POPM + +# import hunter +# hunter.trace(action=hunter.CallPrinter) + + +def mutagen_inspect(filename): + os.system(f"mutagen-inspect {filename} | grep POP") + + +def clear_popm(filename): + id3 = ID3(filename) + id3.delall('POPM') + id3.save() + + +def clear_priv(filename): + id3 = ID3(filename) + id3.delall('PRIV') + id3.save() + + +def set_popm_mutagen(filename, email, rating=None, count=None): + id3 = ID3(filename) + id3['POPM'] = POPM(email=email, rating=rating, count=count) + id3.save() + + +def set_popm_mediafile(filename, email, rating, count): + mf = MediaFile(filename) + mf.popm = POPM(email=email, rating=rating, count=count) + mf.save() + + +def get_popm_mutagen(filename): + id3 = ID3(filename) + return id3.getall('POPM') + + +def get_popm_mediafile(filename): + mf = MediaFile(filename) + return mf.popm + + +if __name__ == '__main__': + filename = 'test/rsrc/popm.mp3' + clear_popm(filename) + clear_priv(filename) + + set_popm_mutagen(filename, email='foo@bar.de', rating=5) + print(get_popm_mutagen(filename)) + + clear_popm(filename) + + # set_popm_mutagen(filename, email='foo@bar.de', count=5) + # print(get_popm_mutagen(filename)) + + # set_popm_mediafile(filename, email='foo@bar.de', rating=5, count=5) + # set_popm_mediafile(filename, email='foo2@bar.de', rating=5, count=5) + # print("after: {}".format(get_popm_mediafile(filename))) + # + # mf = MediaFile(filename) + # from ipdb import set_trace; set_trace() \ No newline at end of file diff --git a/mediafile.py b/mediafile.py index 51f352a..287f2d4 100644 --- a/mediafile.py +++ b/mediafile.py @@ -42,6 +42,7 @@ import mutagen.asf import codecs +import collections import datetime import re import base64 @@ -56,7 +57,8 @@ import six -__all__ = ['UnreadableFileError', 'FileTypeError', 'MediaFile'] +__all__ = ['UnreadableFileError', 'FileTypeError', 'MediaFile', + 'Popularimeter'] log = logging.getLogger(__name__) @@ -78,6 +80,10 @@ PREFERRED_IMAGE_EXTENSIONS = {'jpeg': 'jpg'} +Popularimeter = collections.namedtuple( + 'Popularimeter', ['email', 'rating', 'count'] +) + # Exceptions. @@ -1222,6 +1228,8 @@ def _none_value(self): return False elif self.out_type == six.text_type: return u'' + elif self.out_type == dict: + return {} class ListMediaField(MediaField): @@ -1414,6 +1422,34 @@ def __delete__(self, mediafile): delattr(mediafile, 'images') +class PopmMediaField(MediaField): + pass + + +class MP3PopmStorageStyle(StorageStyle): + formats = ['MP3'] + + def get(self, mutagen_file): + """Returns POPM dictionary: {EMAIL: {'rating': RATING, 'count': COUNT}} + """ + return { + popm.email: {'rating': popm.rating, 'count': popm.count} + for popm in mutagen_file.tags.getall('POPM') + } + + def set(self, mutagen_file, value): + """Set POPM. 'count' will be set to 0 by default.""" + popm_list = [ + mutagen.id3.POPM( + email=email, + rating=value[email]['rating'], + count=value[email].get('count', 0) + ) + for email in value.keys() + ] + mutagen_file.tags.setall('POPM', popm_list) + + class QNumberField(MediaField): """Access integer-represented Q number fields. @@ -1849,6 +1885,10 @@ def update(self, dict): StorageStyle('MUSICBRAINZ_ALBUMCOMMENT'), ASFStorageStyle('MusicBrainz/Album Comment'), ) + popm = PopmMediaField( + MP3PopmStorageStyle(key='POPM', as_type=list), + out_type=dict + ) # Release date. date = DateField( @@ -2090,6 +2130,17 @@ def update(self, dict): ASFStorageStyle('INITIALKEY'), ) + # @property + # def popm(self): + # # return {} + # return self._popm + # + # @popm.setter + # def popm(self, value): + # self._popm = value + # # from ipdb import set_trace; set_trace() + # # pass + @property def length(self): """The duration of the audio in seconds (a float).""" diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..07f7f65 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,3 @@ +hunter==3.0.5 +ipdb==0.12.3 +ipython==7.10.1 diff --git a/snippets.txt b/snippets.txt new file mode 100644 index 0000000..9ba39d0 --- /dev/null +++ b/snippets.txt @@ -0,0 +1,5 @@ + popm_tags = mediafile.mgfile.tags.getall('POPM') + return { + tag.email: Popularimeter(email=tag.email, rating=tag.rating, count=tag.count) + for tag in popm_tags + } \ No newline at end of file diff --git a/test/rsrc/popm.mp3 b/test/rsrc/popm.mp3 new file mode 100755 index 0000000..9fa00a3 Binary files /dev/null and b/test/rsrc/popm.mp3 differ diff --git a/test/test_mediafile.py b/test/test_mediafile.py index e9e1850..f663a19 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -23,11 +23,12 @@ import datetime import time import unittest + from six import assertCountEqual from test import _common -from mediafile import MediaFile, Image, \ - ImageType, CoverArtField, UnreadableFileError +from mediafile import MediaFile, Image, ImageType, CoverArtField, \ + UnreadableFileError class ArtTestMixin(object): @@ -755,6 +756,37 @@ def test_unknown_apic_type(self): mediafile = self._mediafile_fixture('image_unknown_type') self.assertEqual(mediafile.images[0].type, ImageType.other) + def test_write_single_popm(self): + mediafile = self._mediafile_fixture('empty') + test_email = 'test1@foobar.de' + mediafile.popm = { + test_email: {'rating': 1, 'count': 1} + } + mediafile.save() + self.assertEqual(mediafile.popm, { + test_email: {'rating': 1, 'count': 1} + }) + + def test_write_popm_without_count(self): + mediafile = self._mediafile_fixture('empty') + test_email = 'test1@foobar.de' + mediafile.popm = {test_email: {'rating': 1}} + mediafile.save() + self.assertEqual(mediafile.popm, { + test_email: {'rating': 1, 'count': 0} + }) + + def test_write_multiple_popm(self): + mediafile = self._mediafile_fixture('full') + mediafile.popm = {'test1@foobar.de': {'rating': 1, 'count': 1}, + 'test2@foobar.de': {'rating': 2, 'count': 2}} + mediafile.save() + + self.assertEqual(mediafile.popm, { + 'test1@foobar.de': {'rating': 1, 'count': 1}, + 'test2@foobar.de': {'rating': 2, 'count': 2}, + }) + class MP4Test(ReadWriteTestBase, PartialTestMixin, ImageStructureTestMixin, unittest.TestCase): @@ -975,7 +1007,7 @@ def test_properties_from_readable_fields(self): def test_known_fields(self): fields = list(ReadWriteTestBase.tag_fields) - fields.extend(('encoder', 'images', 'genres', 'albumtype')) + fields.extend(('encoder', 'images', 'genres', 'albumtype', 'popm')) assertCountEqual(self, MediaFile.fields(), fields) def test_fields_in_readable_fields(self):