Edição de arquivos em massa com ranger

Traduções: en
20/10/2020

Meu gerenciador de arquivos é o ranger. Ele é de terminal, permite remapear todos os comandos me permitindo ser mais eficiente em navegar pelos meus arquivos, e é incrivelmente extensível já que permite a criação de comandos customizados em python. Se isso não bastasse, ele também tem um monte de outras funcionalidades (visualização de arquivos extensível, abas, rótulos, ...). Vá checar a página do GitHub deles, sério.

Uma funcionalidade muito útil do ranger é o comando bulkrename. Ele permite que você abra um editor com o nome de todos os arquivos selecionados e os edite. Depois de salvar, um script de shell é gerado para realizar as renomeações necessárias (e te dá a chance de revisá-lo), e como se fosse mágica *puff* você acabou de renomear um monte de arquivos simultaneamente da conveniência do seu editor de texto preferido.

Veja um exemplo:

Comando bulkrename do ranger sendo usado para renomear três arquivos de uma só vez

Em asciinema.org

Bem conveniente. Mas se você parar para pensar, é bem limitado. Por que permitir só renomeação? O mesmo funcionamento poderia ser usado para editar qualquer atributo do arquivo. Só é necessário fornecer uma forma de obter o atributo para cada arquivo selecionado e de gerar um comando de shell que mude o atributo para aplicar a mudança feita pelo usuário.

O comando bulk

Recentemente eu quis modificar o rótulo ID3 Artista de múltiplos arquivos mp3 ao mesmo tempo o que me motivou a escrever a versão genérica do bulkrename do ranger: bulk.

Para isso eu basicamente copiei o código do bulkrename e generalizei a obtenção do atributo do arquivo e a geração do comando de modificação do atributo, chamando get_attribute() e get_change_attribute_cmd(), respectivamente, de um subcomando bulk armazenado no dicionário bulk.

A classe do comando bulk é a seguinte:

class bulk(Command):
    """:bulk <attribute>

    This command opens a list with the attribute <attribute> for each of the
    selected files in an external editor.
    After you edit and save the file, it will generate a shell script
    which changes the attributes in bulk according to the changes you did in the
    file.

    This shell script is opened in an editor for you to review.
    After you close it, it will be executed.
    """


    bulk = {}

    def execute(self):  # pylint: disable=too-many-locals,too-many-statements
        import sys
        import tempfile
        from ranger.container.file import File
        from ranger.ext.shell_escape import shell_escape as esc
        py3 = sys.version_info[0] >= 3

        # get bulk command argument
        bkname = self.rest(1)

        # Create and edit the file list
        files = [f for f in self.fm.thistab.get_selection()]
        attributes = [self.bulk[bkname].get_attribute(f) for f in files]
        listfile = tempfile.NamedTemporaryFile(delete=False)
        listpath = listfile.name

        if py3:
            listfile.write("\n".join(attributes).encode("utf-8"))
        else:
            listfile.write("\n".join(attributes))
        listfile.close()
        self.fm.execute_file([File(listpath)], app='editor')
        listfile = open(listpath, 'r')
        new_attributes = listfile.read().split("\n")
        listfile.close()
        os.unlink(listpath)
        if all(a == b for a, b in zip(attributes, new_attributes)):
            self.fm.notify("Nothing to be done!")
            return
        print(new_attributes)

        # Generate script
        cmdfile = tempfile.NamedTemporaryFile()
        script_lines = []
        script_lines.append("# This file will be executed when you close the editor.\n")
        script_lines.append("# Please double-check everything, clear the file to abort.\n")
        script_lines.extend("%s\n" % self.bulk[bkname].get_change_attribute_cmd(file, old, new)
                            for old, new, file in
                            zip(attributes, new_attributes, files) if old != new)
        script_content = "".join(script_lines)
        if py3:
            cmdfile.write(script_content.encode("utf-8"))
        else:
            cmdfile.write(script_content)
        cmdfile.flush()

        # Open the script and let the user review it
        self.fm.execute_file([File(cmdfile.name)], app='editor')
        cmdfile.seek(0)

        # Do the attribute changing
        self.fm.run(['/bin/sh', cmdfile.name], flags='w')
        cmdfile.close()

Então, dentro dessa classe, eu adicionei uma classe para cada um dos subcomandos bulk que eu queria adicionar: id3art, id3tit e id3alb, que modificam o rótulo ID3 para o Artista, Título e Álbum, respectivamente.

Para cada um deles, eu implementei os métodos get_attribute() e get_change_attribute_cmd(). O método get_attribute() recebe o objeto de arquivo do ranger e deve retornar uma string com o atributo (no caso do id3art, o rótulo ID3 Artista, o qual foi obtido usando o módulo python eyed3). O método get_change_attribute_cmd() recebe o objeto de arquivo do ranger, o atributo antigo (o retornado por get_attribute()) e o novo (o valor editado pelo usuário), e deve retornar uma string contendo o comando de shell para aplicar a mudança feita pelo usuário (no caso do id3art, eyeD3 -a NovoArtista nomeDoArquivo.mp3).

Finalmente, eu também adicionei uma entrada para cada um desses subcomandos no dicionário bulk, que mapeia o nome do subcomando ao seu objeto.

Traduzindo tudo isso para código, foi isso o que eu adicionei dentro da classe bulk:

class id3art(object):

    def get_attribute(self, file):
        import eyed3
        artist = eyed3.load(file.relative_path).tag.artist
        return str(artist) if artist else ''


    def get_change_attribute_cmd(self, file, old, new):
        from ranger.ext.shell_escape import shell_escape as esc
        return "eyeD3 -a %s %s" % (esc(new), esc(file))

class id3tit(object):

    def get_attribute(self, file):
        import eyed3
        title = eyed3.load(file.relative_path).tag.title
        return str(title) if title else ''


    def get_change_attribute_cmd(self, file, old, new):
        from ranger.ext.shell_escape import shell_escape as esc
        return "eyeD3 -t %s %s" % (esc(new), esc(file))

class id3alb(object):

    def get_attribute(self, file):
        import eyed3
        album = eyed3.load(file.relative_path).tag.album
        return str(album) if album else ''


    def get_change_attribute_cmd(self, file, old, new):
        from ranger.ext.shell_escape import shell_escape as esc
        return "eyeD3 -A %s %s" % (esc(new), esc(file))

bulk = {'id3art': id3art(),
        'id3tit': id3tit(),
        'id3alb': id3alb(),
        }

E aqui está ele em ação:

O subcomando bulk id3art sendo usado para mudar o rótulo ID3 Artista de três arquivos mp3 de uma vez só

Em asciinema.org

Já que eu percebi que esse funcionamento genérico do comando bulk poderia ser útil para todos os usuários do ranger, cada um implementando seu próprio subcomando bulk, eu sugeri a adição da funcionalidade. Aparentemente a ideia foi bem aceita, e inclusive há a intenção de tornar o bulkrename apenas um apelido para um subcomando bulk, então talvez em um futuro próximo você possa criar seu próprio subcomando bulk usando o comando bulk já integrado ao ranger 🙂.