Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,15 @@ Warning: failed to verify integrity
Do you want to skip this album? (Y/n)
```

Alteratively, if auto fixing is enabled,
then _beets-check_ will fix the file using the configured tool.
If the fix is successful, then the import will continue as normal.
Otherwise, it will prompt you in a similar way above,
where you can choose to continue or skip the offending items.

After a track has been added to the database and all modifications to the tags
have been written, beets-check adds the checksums. This is virtually the same as
running `beets check -a ` after the import.
running `beets check -a` after the import.

If you run `import` with the `--quiet` flag the importer will skip
files that do not pass third-party tests automatically and log an
Expand Down Expand Up @@ -237,6 +243,8 @@ check:
write-check: yes
write-update: yes
convert-update: yes
integrity: yes
auto-fix: no
threads: num_of_cpus
```

Expand All @@ -252,6 +260,8 @@ other beets commands. You can disable each option by setting its value to `no`.
`beet write` or `beet modify`.
- `convert-update: no` Don’t updated the checksum if a file has been
converted with the `--keep-new` flag.
- `integrity: no` Don't preform integrity checks on import
- `auto-fix: yes` Automatically fix files on import that fail the integrity check
Comment thread
OwenCochell marked this conversation as resolved.
Outdated
- `threads: 4` Use four threads to compute checksums.

### Third-party Tools
Expand Down
52 changes: 48 additions & 4 deletions beetsplug/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def __init__(self):
"import": True,
"write-check": True,
"write-update": True,
"auto-fix": False, # Will automatically fix integrity errors on import
Comment thread
OwenCochell marked this conversation as resolved.
Outdated
"integrity": True,
"convert-update": True,
"threads": os.cpu_count(),
Expand All @@ -75,9 +76,12 @@ def __init__(self):
"fix": "mp3val -nb -f {0}",
},
"flac": {
"cmdline": "flac --test --silent {0}",
# More aggresive check by default
"cmdline": "flac --test --silent --warnings-as-errors {0}",
"formats": "FLAC",
"error": "^.*: ERROR,? (.*)$",
"error": "^.*: (?:WARNING|ERROR),? (.*)$",
# Recodes and fixes errors
"fix": "flac -VFf --preserve-modtime -o \"{0}\" \"${0}\""
},
"oggz-validate": {"cmdline": "oggz-validate {0}", "formats": "OGG"},
},
Expand Down Expand Up @@ -139,21 +143,61 @@ def copy_original_checksum(self, config, task):

def verify_import_integrity(self, session, task):
integrity_errors = []
failed_items = []
if not task.items:
return
for item in task.items:
try:
verify_integrity(item)
except IntegrityError as ex:
integrity_errors.append(ex)
failed_items.append(item)
Comment thread
OwenCochell marked this conversation as resolved.
Outdated

if integrity_errors:

# If True, then all errors have been corrected and we
# do not need to prompt the user at the end of this branch
# If this is False, then we did not fix the problem,
# and the user should still be prompted to skip the album
fixed: bool = False

log.warning("Warning: failed to verify integrity")
for error in integrity_errors:
log.warning(f" {displayable_path(item.path)}: {error}")
if beets.config["import"]["quiet"] or input_yn(
if self.config["auto-fix"]:
log.info("Attempting to fix files...")

# Iniitally, we assume we can fix all files,
# so we set fixed to True. This will change if we fail to fix

fixed = True
Comment thread
geigerzaehler marked this conversation as resolved.
Outdated

# TODO: Only gets a checker for the first item,
# could fail if multiple formats are present.
checker = IntegrityChecker.fixer(failed_items[0])
if checker:
for item in failed_items:
try:
checker.fix(item)
item["checksum"] = compute_checksum(item)
log.info(f"Fixed {displayable_path(item.path)}")
except Exception as e:
log.error(
f"Failed to fix {displayable_path(item.path)}: {e}")

# We failed to fix, so we need to prompt the user
# We also stop proecessing further files
fixed = False
break
else:
log.error("No integrity fixer available.")
Comment thread
geigerzaehler marked this conversation as resolved.
Outdated

# no integrity fixer available, so we need to prompt the user
fixed = False

if beets.config["import"]["quiet"] or (not fixed and input_yn(
"Do you want to skip this album (Y/n)"
):
)):
log.info("Skipping.")
task.choice_flag = importer.action.SKIP

Expand Down
Binary file added test/fixtures/fail.mp3
Binary file not shown.
23 changes: 22 additions & 1 deletion test/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,5 +246,26 @@ def installNone(cls):
check.IntegrityChecker._all_available = []

def check(self, item):
if b"truncated" in item.path:
if b"truncated" in item.path or b'fail' in item.path:
raise check.IntegrityError(item.path, "file is corrupt")

def can_fix(self, item):
# We always attempt to fix the file
return True

def fix(self, item):

# We check for the presence of title tag 'truncated' in file

mf = MediaFile(item.path)

if b'truncated' in item.path:
# "Fix" the file by chaning the tag
# We store the 'fixed' value in the URL tag,
# which can then be checked in the test

mf.url = "fixed"
mf.save()

if b"fail" in item.path:
raise check.IntegrityError(item.path, "cannot fix file")
93 changes: 93 additions & 0 deletions test/integration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,99 @@ def test_add_corrupt_files(self):
mediafile = MediaFile(item.path)
assert mediafile.title == "truncated tag"

def test_fix_corrupt_files(self):

# Enable auto-fix in config

self.config["check"]["auto-fix"] = True

MockChecker.install()
self.setupImportDir(["ok.mp3", "truncated.mp3"])

with self.mockAutotag(), captureLog() as logs:
beets.ui._raw_main(["import", self.import_dir])

assert len(self.lib.items()) == 2

assert "Attempting to fix files..." in "\n".join(logs)
assert "Fixed" in "\n".join(logs)

item = self.lib.items("truncated").get()

# The checkum should match the file

verify_checksum(item)

# Ensure this really is the same file that was broken originally,
# We do this by check to see if the title tag was changed to something good

mediafile = MediaFile(item.path)

# Did we actually fix the file?
assert mediafile.url == "fixed"

def test_fix_corrupt_files_fail_skip(self):

# Enable auto-fix in config

self.config["check"]["auto-fix"] = True

MockChecker.install()
self.setupImportDir(["ok.mp3", "fail.mp3"])

with self.mockAutotag(), captureLog() as logs, controlStdin("y"):
beets.ui._raw_main(["import", self.import_dir])

# We have skipped this import, so library should be empty

assert len(self.lib.items()) == 0

assert "Attempting to fix files..." in "\n".join(logs)
assert "Failed to fix" in "\n".join(logs)

def test_fix_corrupt_files_fail(self):

# Enable auto-fix in config

self.config["check"]["auto-fix"] = True

MockChecker.install()
self.setupImportDir(["ok.mp3", "fail.mp3"])

with self.mockAutotag(), captureLog() as logs, controlStdin("n"):
beets.ui._raw_main(["import", self.import_dir])

# We have imported this library anyway, so library should contain both items

assert len(self.lib.items()) == 2

assert "Attempting to fix files..." in "\n".join(logs)
assert "Failed to fix" in "\n".join(logs)

def test_fix_corrupt_files_quiet(self):

# Enable auto-fix in config

self.config["check"]["auto-fix"] = True

# Enable quite mode, should not import files by default

self.config["import"]["quiet"] = True

MockChecker.install()
self.setupImportDir(["ok.mp3", "fail.mp3"])

with self.mockAutotag(), captureLog() as logs:
beets.ui._raw_main(["import", self.import_dir])

# Since import failed, and quite mode is enabled,
# we should not import anything

assert len(self.lib.items()) == 0

assert "Attempting to fix files..." in "\n".join(logs)
assert "Failed to fix" in "\n".join(logs)


class WriteTest(TestHelper, TestCase):
def setUp(self):
Expand Down
Loading