Last active
August 23, 2019 21:51
-
-
Save lpinner/6851497d204c9ac75d3400a1b5e862d8 to your computer and use it in GitHub Desktop.
Batch file renamer with regex and glob style searching
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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_()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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><a href='https://docs.python.org/3/howto/regex.html'>Regex?</a></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