Skip to content

Instantly share code, notes, and snippets.

@lpinner
Last active August 23, 2019 21:51
Show Gist options
  • Save lpinner/6851497d204c9ac75d3400a1b5e862d8 to your computer and use it in GitHub Desktop.
Save lpinner/6851497d204c9ac75d3400a1b5e862d8 to your computer and use it in GitHub Desktop.
Batch file renamer with regex and glob style searching
#!/usr/bin/python3
import os, sys
from urllib.parse import unquote, urlsplit
import fnmatch, re, sre_constants
from PyQt5 import uic
from PyQt5.QtCore import QObject, Qt
from PyQt5.QtWidgets import (
QWidget, QApplication, QDesktopWidget,
QHeaderView, QStyle, QTableWidgetItem, QMainWindow)
class MainWindow(QMainWindow):
def __init__(self, files):
super().__init__()
files = [unquote(urlsplit(f).path) for f in files]
self.files = [f for f in files if os.path.exists(f)]
self.new_files = {}
self.load_ui()
self.add_files()
self.ui.show()
def add_files(self):
ui = self.ui
flags = Qt.ItemFlags()
flags &= Qt.ItemIsEnabled
flags &= QStyle.State_Active
flags &= QStyle.State_Enabled
for i, f in enumerate(self.files):
ui.preview_table.insertRow(i)
item = QTableWidgetItem(os.path.basename(f))
item.setFlags(flags)
ui.preview_table.setItem(i, 0, item)
ui.preview_table.setItem(i, 1, QTableWidgetItem(''))
def load_ui(self):
uifilename = __file__.replace('.py', '.ui')
ui = uic.loadUi(uifilename)
header = ui.preview_table.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.Stretch)
header.setSectionResizeMode(1, QHeaderView.Stretch)
ui.find_text.textChanged.connect(self.on_preview)
ui.replace_text.textChanged.connect(self.on_preview)
ui.regex_checkbox.clicked.connect(self.on_preview)
ui.case_checkbox.clicked.connect(self.on_preview)
ui.okclose_buttonbox.accepted.connect(self.on_ok)
ui.okclose_buttonbox.rejected.connect(self.on_cancel)
rect = ui.frameGeometry()
centre = QDesktopWidget().availableGeometry().center()
rect.moveCenter(centre)
ui.move(rect.topLeft())
self.ui = ui
def find_replace(self, find, replace, is_regex, case_sensitive):
if not is_regex:
find = fnmatch.translate(find).replace(r'\Z','')
#todo regex error handling
try:
flags = 0
if not case_sensitive:
flags = re.IGNORECASE
regex = re.compile(find, flags)
except sre_constants.error:
return (None, None, None)
for fp in self.files:
f,e = os.path.splitext(os.path.basename(fp))
try:
f = regex.sub(replace, f)
except sre_constants.error:
yield (None, None, None)
fn = os.path.join(os.path.dirname(fp), f+e)
yield (fp, fn, f+e)
def update_previews(self):
ui = self.ui
self.new_files = {}
find = ui.find_text.text()
if not find:
for i in range(len(self.files)):
ui.preview_table.setItem(i, 1, QtGui.QTableWidgetItem(''))
replace = ui.replace_text.text()
is_regex = ui.regex_checkbox.isChecked()
case_sensitive = ui.case_checkbox.isChecked()
for i, (fp, fn, fe) in enumerate(self.find_replace(find, replace, is_regex, case_sensitive)):
print(repr((fp, fn, fe)))
if fp is None or fp == fn:
# continue
ui.preview_table.setItem(i, 1, QTableWidgetItem(''))
if fn and fn != fp and not os.path.exists(fn):
ui.preview_table.setItem(i, 1, QTableWidgetItem(fe))
self.new_files[fp] = fn
def on_cancel(self, *args):
self.ui.close()
def on_ok(self, *args):
for fp, fn in self.new_files.items():
print(' '.join([fp, fn]))
os.rename(fp, fn)
self.ui.close()
def on_preview(self, *args):
self.update_previews()
if __name__ == "__main__":
try:
files = sys.argv[1:]
except:
sys.exit(1)
app = QApplication(sys.argv)
mainwindow = MainWindow(files)
sys.exit(app.exec_())
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<author>Luke Pinner</author>
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>659</width>
<height>391</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QGridLayout" name="gridLayout_2">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="findLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Find</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="find_text"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="replaceLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Replace</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="replace_text"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="regexLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Regex</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="previewLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Preview</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QTableWidget" name="preview_table">
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<property name="lineWidth">
<number>1</number>
</property>
<attribute name="horizontalHeaderCascadingSectionResizes">
<bool>true</bool>
</attribute>
<attribute name="horizontalHeaderDefaultSectionSize">
<number>50</number>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>false</bool>
</attribute>
<attribute name="verticalHeaderCascadingSectionResizes">
<bool>false</bool>
</attribute>
<attribute name="verticalHeaderStretchLastSection">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>Original</string>
</property>
<property name="textAlignment">
<set>AlignJustify|AlignVCenter</set>
</property>
</column>
<column>
<property name="text">
<string>Preview</string>
</property>
<property name="textAlignment">
<set>AlignTrailing|AlignVCenter</set>
</property>
</column>
</widget>
</item>
<item row="4" column="1">
<widget class="QDialogButtonBox" name="okclose_buttonbox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
<item row="2" column="1">
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QCheckBox" name="regex_checkbox">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="0" column="3">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="2">
<widget class="QCheckBox" name="case_checkbox">
<property name="text">
<string/>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="case_label">
<property name="text">
<string>Case Sensitive</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label">
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<property name="text">
<string>&lt;a href='https://docs.python.org/3/howto/regex.html'&gt;Regex?&lt;/a&gt;</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<includes>
<include location="local"># Copyright 2016 Luke Pinner</include>
<include location="local"># </include>
<include location="local"># Licensed under the Apache License, Version 2.0 (the License);</include>
<include location="local"># you may not use this file except in compliance with the License.</include>
<include location="local"># You may obtain a copy of the License at</include>
<include location="local"># </include>
<include location="local"># http://www.apache.org/licenses/LICENSE-2.0</include>
<include location="local"># </include>
<include location="local"># Unless required by applicable law or agreed to in writing, software</include>
<include location="local"># distributed under the License is distributed on an AS IS BASIS,</include>
<include location="local"># WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.</include>
<include location="local"># See the License for the specific language governing permissions and</include>
<include location="local"># limitations under the License.</include>
</includes>
<resources/>
<connections/>
</ui>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment