|
| 1 | +# This file is part of beets. |
| 2 | +# Copyright 2025, Rebecca Turner. |
| 3 | +# |
| 4 | +# Permission is hereby granted, free of charge, to any person obtaining |
| 5 | +# a copy of this software and associated documentation files (the |
| 6 | +# "Software"), to deal in the Software without restriction, including |
| 7 | +# without limitation the rights to use, copy, modify, merge, publish, |
| 8 | +# distribute, sublicense, and/or sell copies of the Software, and to |
| 9 | +# permit persons to whom the Software is furnished to do so, subject to |
| 10 | +# the following conditions: |
| 11 | +# |
| 12 | +# The above copyright notice and this permission notice shall be |
| 13 | +# included in all copies or substantial portions of the Software. |
| 14 | +"""Detect missing files, folders, and album art.""" |
| 15 | + |
| 16 | +from __future__ import annotations |
| 17 | + |
| 18 | +import os |
| 19 | +from pathlib import Path |
| 20 | +from typing import TYPE_CHECKING |
| 21 | + |
| 22 | +from beets import plugins, ui, util |
| 23 | + |
| 24 | +if TYPE_CHECKING: |
| 25 | + import optparse |
| 26 | + |
| 27 | + from beets.library import Library |
| 28 | + |
| 29 | + |
| 30 | +class DetectMissingPlugin(plugins.BeetsPlugin): |
| 31 | + def __init__(self) -> None: |
| 32 | + super().__init__() |
| 33 | + |
| 34 | + def commands(self) -> list[ui.Subcommand]: |
| 35 | + cmd = ui.Subcommand( |
| 36 | + "detectmissing", help="Detect missing files, folders, and album art" |
| 37 | + ) |
| 38 | + |
| 39 | + cmd.parser.add_option( |
| 40 | + "--delete", |
| 41 | + help="Also delete missing items from the library", |
| 42 | + action="store_true", |
| 43 | + ) |
| 44 | + |
| 45 | + cmd.func = self.detect_missing |
| 46 | + |
| 47 | + return [cmd] |
| 48 | + |
| 49 | + def detect_missing( |
| 50 | + self, lib: Library, opts: optparse.Values, _args: list[str] |
| 51 | + ) -> None: |
| 52 | + should_delete: bool = opts.delete or False |
| 53 | + |
| 54 | + for album in lib.albums(): |
| 55 | + art_filepath = album.art_filepath |
| 56 | + if art_filepath is not None and not art_filepath.exists(): |
| 57 | + print(f"{art_filepath}") |
| 58 | + |
| 59 | + if should_delete: |
| 60 | + with lib.transaction(): |
| 61 | + album.artpath = None |
| 62 | + album.store(fields=["artpath"]) |
| 63 | + |
| 64 | + for item in lib.items(): |
| 65 | + if item.path is not None: |
| 66 | + path = Path(os.fsdecode(item.path)) |
| 67 | + if not path.exists(): |
| 68 | + print(f"{path}") |
| 69 | + |
| 70 | + if should_delete: |
| 71 | + util.prune_dirs( |
| 72 | + os.path.dirname(item.path), lib.directory |
| 73 | + ) |
| 74 | + with lib.transaction(): |
| 75 | + item.remove(delete=False, with_album=True) |
0 commit comments