diff --git a/beets/attachments.py b/beets/attachments.py index e63fc731e..84a5fa7a5 100644 --- a/beets/attachments.py +++ b/beets/attachments.py @@ -605,6 +605,7 @@ class AttachmentFactory(object): Uses the functions from `register_detector` and the `attachments.types` configuration. """ + # TODO Make list unique for detector in self._detectors: try: type = detector(path) diff --git a/docs/guides/attachments.rst b/docs/guides/attachments.rst new file mode 100644 index 000000000..86d394843 --- /dev/null +++ b/docs/guides/attachments.rst @@ -0,0 +1,194 @@ +Attachments +=========== + +Beets also gives you tools to organize the non-audio files that are +part of your music collection, e.g. cover images, digital booklets, rip +logs, etc. These are called *attachments*. Each attachment has at least +a path and a type attribute, and a track or album (an *entity*) it is +attached to. The type attribute provides a basic taxonomy for your +attachments and allows plugins to provide additional functionality for +specific types. + + +Getting Started +--------------- + +TODO: Introduction + + +Attaching Single Files +^^^^^^^^^^^^^^^^^^^^^^ + +Suppose you have downloaded the cover of the Beatles’ “Revolver” album +and the file is called `cover.jpg`. You can attach it to the album with +the following command: :: + + $ beet attach /path/to/cover.jpg --type cover album:Revolver + add cover attachment /path/to/cover.jpg to 'The Beatles - Revolver' + +The file at ``/path/to/cover.jpg`` has now been moved to the album +directory and you can query the attachment with :: + + $ beet attachls type:cover e:album:Revolver + cover: /music/Revolver/cover.jpg + +The query arguments for that `attachls` command work like the arguments +for the usual :ref:`ls ` command, with one addition: You can match against +the album or track (the *entity*) the file is attached to using the +``e:`` prefix. For more on the `attachls` command see :ref:`the +reference `. + +Maybe you want your cover images to have a different name, say +`front.jpg`. You can change the default paths for you attachments +through the configuration file: :: + + attachments: + paths: + - type: cover + path: front.$ext + +This moves all attachments of type cover are to `front.ext` in the +corresponding album directory, where `ext` is the extension of the +source file. :: + + $ beet attach /path/to/cover.jpg --type cover album:Revolver + add cover attachment /path/to/cover.jpg to 'The Beatles - Revolver' + + $ beet attachls type:cover e:album:Revolver + cover: /music/Revolver/front.jpg + + +Beets can also be configured to automatically detect the type of an +attachment from its filename. :: + + attachments: + types: + cover.*: cover + +The :ref:`types configuration ` is a map from +glob patterns or regular expressions to type names. You can now omit +the ``--type`` option and beet will detect the type automatically :: + + $ beet attach /path/to/cover.jpg album:Revolver + add cover attachment /path/to/cover.jpg to 'The Beatles - Revolver' + + $ beet attachls type:cover e:album:Revolver + cover: /music/Revolver/cover.jpg + +Of course you can still specify another type on the command line. + + +Importing Attachments +^^^^^^^^^^^^^^^^^^^^^ + +Beets will automatically create attachments when you import new music. + +Since you already have “Revolver” in your library, suppose you want to +also add the “Abbey Road” album. You have ripped the album and moved it +to the ``/import`` directory. The directory also contains the files +``cover.jpg`` and ``booklet.pdf`` that you want to create attachments +for. To automatically detect the types, we add the following to our +configuration: :: + + attachments: + types: + cover.*: cover + booklet.pdf: booklet + +In addition to adding the album to your library, the ``beet import +/import`` command will now print the lines :: + + add cover attachment /import/cover.jpg to 'The Beatles - Revolver' + add booklet attachment /import/booklet.pdf to 'The Beatles - Revolver' + +and you can confirm it with :: + + $ beet attachls "e:Abbey Road" + /music/Abbey Road/cover.jpg + /music/Abbey Road/booklet.pdf + +For each album that is about to be imported, beets looks at all the +non-music files contained in the album’s source directory. Beets then +tries to determine the type of each file and, if successful, creates an +attachment of this type. Files with no type are ignored. The file +manipulations for attachments mirror that of the music files and can be +configured through the ``import.move`` and ``import.copy`` options. + + +Import Attachments Only +^^^^^^^^^^^^^^^^^^^^^^^ + +If you have used beets before, you may already have some files in your +library that you want to attach with beets. Instead of repeating the +`attach` command for each of those files, there is a :ref:`attach-import +command `. This command is similar to a reimport +with ``beet import``, but it just creates attachments and skip all +audio files. + +As an example, suppose you have a ``cover.jpg`` file in some of your +album directories and you want them to be added as a ``cover`` +attachment to their corresponding album. First make sure the type of +the file is recognised by beets. :: + + attachments: + types: + cover.jpg: cover + +Then run :: + + $ beet attach-import + add cover attachment /music/Revolver/cover.jpg to 'The Beatles - Revolver' + add cover attachment /music/Abbey Road/cover.jpg to 'The Beatles - Abbey Road' + ... + +and all cover images will be attached to their albums. + + +.. _attachment-plugins: + +Attachment Plugins +------------------ + +TODO + + +Reference +========= + + +Command-Line +------------ + +``attach`` +^^^^^^^^^^ + +``attachls`` +^^^^^^^^^^^^ + +``attach-import`` +^^^^^^^^^^^^^^^^^ + +Configuration +------------- + +.. _conf-attachments-types: + +types +^^^^^ + +paths +^^^^^ + + +To Do +===== + +* Fallback type for discover and import +* Ignore dot files +* Interactive type input on import (create issue) +* Documentation for multiple types (do we need them) +* Document track attachments +* Move attachments with same path +* Automatically determine query from path for `attach` +* Remove warning for unknown files +* Additional template variables overwritten by flex attrs diff --git a/docs/guides/index.rst b/docs/guides/index.rst index 9269b249e..4e022fd87 100644 --- a/docs/guides/index.rst +++ b/docs/guides/index.rst @@ -11,4 +11,5 @@ guide. main tagger advanced + attachments migration diff --git a/test/test_attachments.py b/test/test_attachments.py index c7f674cbb..4c917cdfd 100644 --- a/test/test_attachments.py +++ b/test/test_attachments.py @@ -39,6 +39,7 @@ class AttachmentTestHelper(TestHelper): return self._factory def touch(self, path, dir=None, content=''): + # TODO move into TestHelper if dir: path = os.path.join(dir, path) @@ -133,7 +134,7 @@ class AttachmentTestHelper(TestHelper): def cli_output(self, *args): with capture_stdout() as output: self.runcli(*args) - return output.getvalue().split('\n') + return [l for l in output.getvalue().split('\n') if l] def libpath(self, *components): components = \ @@ -141,6 +142,64 @@ class AttachmentTestHelper(TestHelper): return os.path.join(self.libdir, *components) +class AttachmentDocTest(unittest.TestCase, AttachmentTestHelper): + """Tests for the guide of the attachment documentation. + """ + + def setUp(self): + self.setup_beets() + self.config['path_formats'] = {'default': '$album/$track $title'} + + def tearDown(self): + self.teardown_beets() + + @unittest.skip + def test_attache_single_file_with_type(self): + self.add_album(name='Revolver') + attachment_path = self.touch('cover.jpg') + + output = self.cli_output('attach', attachment_path, '--type', + 'cover', 'album:Revolver') + self.assertIn("add cover attachment {0} to 'The Beatles - Revolver'" + .format(attachment_path), output) + + output = self.cli_output('attachls', 'type:cover', 'e:album:Revolver') + self.assertIn('cover: {0}/Revolver/cover.jpg' + .format(self.libdir), output) + + def test_attache_single_file_with_type_and_path_config(self): + self.config['attachments']['paths'] = [{ + 'type': 'cover', + 'path': 'front.$ext', + }] + self.add_album(name='Revolver') + attachment_path = self.touch('cover.jpg') + + self.runcli('attach', attachment_path, '--type', + 'cover', 'album:Revolver') + + output = self.cli_output('attachls', 'type:cover', 'e:album:Revolver') + self.assertIn('cover: {0}/Revolver/front.jpg' + .format(self.libdir), output) + + @unittest.skip + def test_import_cover_and_booklet(self): + importer = self.create_importer() + album_dir = os.path.join(self.importer.paths[0], 'album 0') + cover_path = self.touch(album_dir, 'cover.jpg') + booklet_path = self.touch(album_dir, 'booklet.pdf') + + with capture_stdout() as output: + importer.run() + output = output.getValue().split('\n') + self.assertIn("add cover attachment {0} to 'Artist - Album 0'" + .format(cover_path), output) + self.assertIn("add booklet attachment {0} to 'Artist - Album 0'" + .format(booklet_path), output) + + # TODO attach-import + + class AttachmentDestinationTest(unittest.TestCase, AttachmentTestHelper): """Test the `attachment.destination` property. """ @@ -263,6 +322,7 @@ class AttachmentTest(unittest.TestCase, AttachmentTestHelper): self.teardown_beets() # attachment.move() + # TODO move attachments with same path def test_move(self): attachment = self.create_album_attachment(self.touch('a')) @@ -424,7 +484,10 @@ class AttachmentFactoryTest(unittest.TestCase, AttachmentTestHelper): self.assertEqual(len(attachments), 1) self.assertEqual(attachments[0].type, 'image') - # TODO Glob and RegExp + # TODO Glob and RegExp. + # * Globs dont match files starting with a dot + # * Add extended bash globs. + # * Regexp must match full basename def test_detect_config_types(self): self.config['attachments']['types'] = { '.*\.jpg': 'image' @@ -601,7 +664,10 @@ class EntityAttachmentsTest(unittest.TestCase, AttachmentTestHelper): class AttachmentImportTest(unittest.TestCase, AttachmentTestHelper): - """Attachments should be created in the importer. + """Import process should discover and add attachments. + + Since the importer uses the `AttachmentFactory.discover()` method more + comprehensive tests can be found in that test case. """ def setUp(self): @@ -651,6 +717,8 @@ class AttachmentImportTest(unittest.TestCase, AttachmentTestHelper): os.path.splitext(item.path)[0] + ' - cover.jpg' ) + # TODO interactive type input + class AttachCommandTest(unittest.TestCase, AttachmentTestHelper): """Tests the `beet attach FILE QUERY...` command