1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
|
import shutil
from pathlib import Path
import mediafile
from beets import ui, util
from beets.library import Item, Library
from beets.plugins import BeetsPlugin
class ReplacePlugin(BeetsPlugin):
def commands(self):
cmd = ui.Subcommand(
"replace", help="replace audio file while keeping tags"
)
cmd.func = self.run
return [cmd]
def run(self, lib: Library, args: list[str]) -> None:
if len(args) < 2:
raise ui.UserError("Usage: beet replace <query> <new_file_path>")
new_file_path: Path = Path(args[-1])
item_query: list[str] = args[:-1]
self.file_check(new_file_path)
item_list = list(lib.items(item_query))
if not item_list:
raise ui.UserError("No matching songs found.")
song = self.select_song(item_list)
if not song:
ui.print_("Operation cancelled.")
return
if not self.confirm_replacement(new_file_path, song):
ui.print_("Aborting replacement.")
return
self.replace_file(new_file_path, song)
def file_check(self, filepath: Path) -> None:
"""Check if the file exists and is supported"""
if not filepath.is_file():
raise ui.UserError(
f"'{util.displayable_path(filepath)}' is not a valid file."
)
try:
mediafile.MediaFile(util.syspath(filepath))
except mediafile.FileTypeError as fte:
raise ui.UserError(fte)
def select_song(self, items: list[Item]):
"""Present a menu of matching songs and get user selection."""
ui.print_("\nMatching songs:")
for i, item in enumerate(items, 1):
ui.print_(f"{i}. {util.displayable_path(item)}")
while True:
try:
index = int(
input(
f"Which song would you like to replace? "
f"[1-{len(items)}] (0 to cancel): "
)
)
if index == 0:
return None
if 1 <= index <= len(items):
return items[index - 1]
ui.print_(
f"Invalid choice. Please enter a number "
f"between 1 and {len(items)}."
)
except ValueError:
ui.print_("Invalid input. Please type in a number.")
def confirm_replacement(self, new_file_path: Path, song: Item):
"""Get user confirmation for the replacement."""
original_file_path: Path = Path(song.path.decode())
if not original_file_path.exists():
raise ui.UserError("The original song file was not found.")
ui.print_(
f"\nReplacing: {util.displayable_path(new_file_path)} "
f"-> {util.displayable_path(original_file_path)}"
)
decision: str = (
input("Are you sure you want to replace this track? (y/N): ")
.strip()
.casefold()
)
return decision in {"yes", "y"}
def replace_file(self, new_file_path: Path, song: Item) -> None:
"""Replace the existing file with the new one."""
original_file_path = Path(song.path.decode())
dest = original_file_path.with_suffix(new_file_path.suffix)
try:
shutil.move(util.syspath(new_file_path), util.syspath(dest))
except Exception as e:
raise ui.UserError(f"Error replacing file: {e}")
if (
new_file_path.suffix != original_file_path.suffix
and original_file_path.exists()
):
try:
original_file_path.unlink()
except Exception as e:
raise ui.UserError(f"Could not delete original file: {e}")
song.path = str(dest).encode()
song.store()
ui.print_("Replacement successful.")
|