File: replace.py

package info (click to toggle)
beets 2.5.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky
  • size: 7,988 kB
  • sloc: python: 46,429; javascript: 8,018; xml: 334; sh: 261; makefile: 125
file content (122 lines) | stat: -rw-r--r-- 3,958 bytes parent folder | download | duplicates (2)
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.")