mirror of
https://github.com/beetbox/beets.git
synced 2025-12-27 11:02:43 +01:00
commit
a0e412076d
7 changed files with 181 additions and 12 deletions
|
|
@ -22,6 +22,8 @@ import logging
|
|||
import pickle
|
||||
import itertools
|
||||
from collections import defaultdict
|
||||
from tempfile import mkdtemp
|
||||
import shutil
|
||||
|
||||
from beets import autotag
|
||||
from beets import library
|
||||
|
|
@ -525,6 +527,11 @@ class ImportTask(object):
|
|||
else:
|
||||
return [self.item]
|
||||
|
||||
def cleanup(self):
|
||||
"""Perform clean up during `finalize` stage.
|
||||
"""
|
||||
pass
|
||||
|
||||
# Utilities.
|
||||
|
||||
def prune(self, filename):
|
||||
|
|
@ -540,6 +547,78 @@ class ImportTask(object):
|
|||
clutter=config['clutter'].as_str_seq())
|
||||
|
||||
|
||||
class ArchiveImportTask(ImportTask):
|
||||
"""Additional methods for handling archives.
|
||||
|
||||
Use when `toppath` points to a `zip`, `tar`, or `rar` archive.
|
||||
"""
|
||||
|
||||
def __init__(self, toppath):
|
||||
super(ArchiveImportTask, self).__init__(toppath)
|
||||
self.sentinel = True
|
||||
self.extracted = False
|
||||
|
||||
@classmethod
|
||||
def is_archive(cls, path):
|
||||
"""Returns true if the given path points to an archive that can
|
||||
be handled.
|
||||
"""
|
||||
if not os.path.isfile(path):
|
||||
return False
|
||||
|
||||
for path_test, _ in cls.handlers():
|
||||
if path_test(path):
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def handlers(cls):
|
||||
"""Returns a list of archive handlers.
|
||||
|
||||
Each handler is a `(path_test, ArchiveClass)` tuple. `path_test`
|
||||
is a function that returns `True` if the given path can be
|
||||
handled by `ArchiveClass`. `ArchiveClass` is a class that
|
||||
implements the same interface as `tarfile.TarFile`.
|
||||
"""
|
||||
if not hasattr(cls, '_handlers'):
|
||||
cls._handlers = []
|
||||
from zipfile import is_zipfile, ZipFile
|
||||
cls._handlers.append((is_zipfile, ZipFile))
|
||||
from tarfile import is_tarfile, TarFile
|
||||
cls._handlers.append((is_tarfile, TarFile))
|
||||
try:
|
||||
from rarfile import is_rarfile, RarFile
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
cls._handlers.append((is_rarfile, RarFile))
|
||||
|
||||
return cls._handlers
|
||||
|
||||
def cleanup(self):
|
||||
"""Removes the temporary directory the archive was extracted to.
|
||||
"""
|
||||
if self.extracted:
|
||||
shutil.rmtree(self.toppath)
|
||||
|
||||
def extract(self):
|
||||
"""Extracts the archive to a temporary directory and sets
|
||||
`toppath` to that directory.
|
||||
"""
|
||||
for path_test, handler_class in self.handlers():
|
||||
if path_test(self.toppath):
|
||||
break
|
||||
|
||||
try:
|
||||
extract_to = mkdtemp()
|
||||
archive = handler_class(self.toppath, mode='r')
|
||||
archive.extractall(extract_to)
|
||||
finally:
|
||||
archive.close()
|
||||
self.extracted = True
|
||||
self.toppath = extract_to
|
||||
|
||||
|
||||
# Full-album pipeline stages.
|
||||
|
||||
def read_tasks(session):
|
||||
|
|
@ -573,6 +652,24 @@ def read_tasks(session):
|
|||
history_dirs = history_get()
|
||||
|
||||
for toppath in session.paths:
|
||||
# Extract archives
|
||||
archive_task = None
|
||||
if ArchiveImportTask.is_archive(syspath(toppath)):
|
||||
if not (config['import']['move'] or config['import']['copy']):
|
||||
log.warn("Cannot import archive. Please set "
|
||||
"the 'move' or 'copy' option.")
|
||||
continue
|
||||
|
||||
log.debug('extracting archive {0}'
|
||||
.format(displayable_path(toppath)))
|
||||
archive_task = ArchiveImportTask(toppath)
|
||||
try:
|
||||
archive_task.extract()
|
||||
except Exception as exc:
|
||||
log.error('extraction failed: {0}'.format(exc))
|
||||
continue
|
||||
toppath = archive_task.toppath
|
||||
|
||||
# Check whether the path is to a file.
|
||||
if not os.path.isdir(syspath(toppath)):
|
||||
try:
|
||||
|
|
@ -627,7 +724,11 @@ def read_tasks(session):
|
|||
yield ImportTask(toppath, paths, items)
|
||||
|
||||
# Indicate the directory is finished.
|
||||
yield ImportTask.done_sentinel(toppath)
|
||||
# FIXME hack to delete extraced archives
|
||||
if archive_task is None:
|
||||
yield ImportTask.done_sentinel(toppath)
|
||||
else:
|
||||
yield archive_task
|
||||
|
||||
# Show skipped directories.
|
||||
if config['import']['incremental'] and incremental_skipped:
|
||||
|
|
@ -937,6 +1038,7 @@ def finalize(session):
|
|||
task.save_progress()
|
||||
if config['import']['incremental']:
|
||||
task.save_history()
|
||||
task.cleanup()
|
||||
continue
|
||||
|
||||
items = task.imported_items()
|
||||
|
|
@ -971,6 +1073,7 @@ def finalize(session):
|
|||
task.save_progress()
|
||||
if config['import']['incremental']:
|
||||
task.save_history()
|
||||
task.cleanup()
|
||||
|
||||
|
||||
# Singleton pipeline stages.
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ New stuff:
|
|||
* There is also a new :doc:`/plugins/keyfinder` that runs a command line
|
||||
program to get the key from audio data and store it in the
|
||||
`initial_key` field.
|
||||
* Beets can now import `zip`, `tar` and `rar` archives.
|
||||
|
||||
Fixes:
|
||||
|
||||
|
|
|
|||
|
|
@ -43,21 +43,25 @@ import
|
|||
``````
|
||||
::
|
||||
|
||||
beet import [-CWAPRqst] [-l LOGPATH] DIR...
|
||||
beet import [-CWAPRqst] [-l LOGPATH] PATH...
|
||||
beet import [options] -L QUERY
|
||||
|
||||
Add music to your library, attempting to get correct tags for it from
|
||||
MusicBrainz.
|
||||
|
||||
Point the command at a directory full of music. The directory can be a single
|
||||
album or a directory whose leaf subdirectories are albums (the latter case is
|
||||
true of typical Artist/Album organizations and many people's "downloads"
|
||||
folders). The music will be copied to a configurable directory structure (see
|
||||
below) and added to a library database (see below). The command is interactive
|
||||
and will try to get you to verify MusicBrainz tags that it thinks are suspect.
|
||||
(This means that importing a large amount of music is therefore very tedious
|
||||
right now; this is something we need to work on. Read the
|
||||
:doc:`autotagging guide </guides/tagger>` if you need help.)
|
||||
Point the command at a directory full of music. The directory can be a
|
||||
single album or a directory whose leaf subdirectories are albums (the
|
||||
latter case is true of typical Artist/Album organizations and many
|
||||
people's "downloads" folders). The path can also be a single file or an
|
||||
archive. Beets supports `zip` and `tar` archives out of the box. To
|
||||
extract `rar` files you need to install the `rarfile`_ package and the
|
||||
`unrar` command. The music will be copied to a configurable directory
|
||||
structure (see below) and added to a library database (see below). The
|
||||
command is interactive and will try to get you to verify MusicBrainz
|
||||
tags that it thinks are suspect. (This means that importing a large
|
||||
amount of music is therefore very tedious right now; this is something
|
||||
we need to work on. Read the :doc:`autotagging guide </guides/tagger>`
|
||||
if you need help.)
|
||||
|
||||
* By default, the command copies files your the library directory and
|
||||
updates the ID3 tags on your music. If you'd like to leave your music
|
||||
|
|
@ -116,6 +120,8 @@ right now; this is something we need to work on. Read the
|
|||
``--group-albums`` option to split the files based on their metadata before
|
||||
matching them as separate albums.
|
||||
|
||||
.. _rarfile: https://pypi.python.org/pypi/rarfile/2.2
|
||||
|
||||
.. only:: html
|
||||
|
||||
Reimporting
|
||||
|
|
|
|||
1
setup.py
1
setup.py
|
|
@ -99,6 +99,7 @@ setup(
|
|||
'echonest_tempo': ['pyechonest'],
|
||||
'lastgenre': ['pylast'],
|
||||
'web': ['flask'],
|
||||
'import': ['rarfile'],
|
||||
},
|
||||
# Non-Python/non-PyPI plugin dependencies:
|
||||
# replaygain: mp3gain || aacgain
|
||||
|
|
|
|||
BIN
test/rsrc/archive.rar
Normal file
BIN
test/rsrc/archive.rar
Normal file
Binary file not shown.
|
|
@ -17,10 +17,13 @@
|
|||
import os
|
||||
import shutil
|
||||
import StringIO
|
||||
from tempfile import mkstemp
|
||||
from zipfile import ZipFile
|
||||
from tarfile import TarFile
|
||||
|
||||
import _common
|
||||
from _common import unittest
|
||||
from helper import TestImportSession, TestHelper
|
||||
from helper import TestImportSession, TestHelper, has_program
|
||||
from beets import library
|
||||
from beets import importer
|
||||
from beets.mediafile import MediaFile
|
||||
|
|
@ -299,6 +302,60 @@ class NonAutotaggedImportTest(_common.TestCase, ImportHelper):
|
|||
self.assertNotExists(os.path.join(self.import_dir, 'the_album'))
|
||||
|
||||
|
||||
class ImportZipTest(unittest.TestCase, ImportHelper):
|
||||
|
||||
def setUp(self):
|
||||
self.setup_beets()
|
||||
|
||||
def tearDown(self):
|
||||
self.teardown_beets()
|
||||
|
||||
def test_import_zip(self):
|
||||
zip_path = self.create_archive()
|
||||
self.assertEqual(len(self.lib.items()), 0)
|
||||
self.assertEqual(len(self.lib.albums()), 0)
|
||||
|
||||
self._setup_import_session(autotag=False, import_dir=zip_path)
|
||||
self.importer.run()
|
||||
self.assertEqual(len(self.lib.items()), 1)
|
||||
self.assertEqual(len(self.lib.albums()), 1)
|
||||
|
||||
def create_archive(self):
|
||||
(handle, path) = mkstemp(dir=self.temp_dir)
|
||||
os.close(handle)
|
||||
archive = ZipFile(path, mode='w')
|
||||
archive.write(os.path.join(_common.RSRC, 'full.mp3'),
|
||||
'full.mp3')
|
||||
archive.close()
|
||||
return path
|
||||
|
||||
|
||||
class ImportTarTest(ImportZipTest):
|
||||
|
||||
def create_archive(self):
|
||||
(handle, path) = mkstemp(dir=self.temp_dir)
|
||||
os.close(handle)
|
||||
archive = TarFile(path, mode='w')
|
||||
archive.add(os.path.join(_common.RSRC, 'full.mp3'),
|
||||
'full.mp3')
|
||||
archive.close()
|
||||
return path
|
||||
|
||||
|
||||
@unittest.skipIf(not has_program('unrar'), 'unrar program not found')
|
||||
class ImportRarTest(ImportZipTest):
|
||||
|
||||
def create_archive(self):
|
||||
return os.path.join(_common.RSRC, 'archive.rar')
|
||||
|
||||
|
||||
@unittest.skip('Implment me!')
|
||||
class ImportPasswordRarTest(ImportZipTest):
|
||||
|
||||
def create_archive(self):
|
||||
return os.path.join(_common.RSRC, 'password.rar')
|
||||
|
||||
|
||||
class ImportSingletonTest(_common.TestCase, ImportHelper):
|
||||
"""Test ``APPLY`` and ``ASIS`` choices for an import session with singletons
|
||||
config set to True.
|
||||
|
|
|
|||
1
tox.ini
1
tox.ini
|
|
@ -14,6 +14,7 @@ deps =
|
|||
flask
|
||||
responses
|
||||
pyechonest
|
||||
rarfile
|
||||
commands =
|
||||
nosetests {posargs}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue