Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"editor.formatOnSave": false,
"ruff.format.backend": "uv"
}
Comment thread
OwenCochell marked this conversation as resolved.
Outdated
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 try to fix files on import with [third-party tools](#third-party-tools)
- `threads: 4` Use four threads to compute checksums.

### Third-party Tools
Expand Down
51 changes: 42 additions & 9 deletions beetsplug/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,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 @@ -81,9 +82,12 @@ def __init__(self):
"fix": "mp3val -nb -f {0}",
},
"flac": {
"cmdline": "flac --test --silent {0}",
# More aggressive 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 @@ -144,21 +148,50 @@ def copy_original_checksum(self, config, task):
item.store()

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((ex, item))

if failed_items:
# If True, then all errors have been corrected
# If this is False, then we did not fix the problem
fixed: bool = False

if integrity_errors:
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(
"Do you want to skip this album (Y/n)"
for error in failed_items:
log.warning(f" {displayable_path(error[1])}: {error[0]}")
if self.config["auto-fix"]:
log.info("Attempting to fix files...")
fixed = True

for item in failed_items:
try:
checker = IntegrityChecker.fixer(item[1])
if not checker:
log.error(
f"No fixer available for file: {displayable_path(item[1].path)}"
)
fixed = False
continue
checker.fix(item[1])
item[1]["checksum"] = compute_checksum(item[1])
log.info(f"Fixed {displayable_path(item[1].path)}")
except Exception as e:
log.error(
f"Failed to fix {displayable_path(item[1].path)}: {e}"
)
# We failed to fix, so we need to prompt the user
# We also stop precessing further files
fixed = False
break

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 = ImporterAction.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 @@ -260,5 +260,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 @@ -112,6 +112,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