loligans
2 years ago
committed by
GitHub
12 changed files with 1340 additions and 28 deletions
@ -0,0 +1,489 @@
@@ -0,0 +1,489 @@
|
||||
<!DOCTYPE html> |
||||
<html lang="${LANG}"> |
||||
|
||||
<head> |
||||
<meta charset="UTF-8" /> |
||||
<title>QBT_TR(Renaming))QBT_TR[CONTEXT=TorrentContentTreeView]</title> |
||||
<script src="scripts/lib/MooTools-Core-1.6.0-compat-compressed.js"></script> |
||||
<script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script> |
||||
<script src="scripts/filesystem.js?v=${CACHEID}"></script> |
||||
<script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script> |
||||
<script src="scripts/file-tree.js?v=${CACHEID}"></script> |
||||
<script src="scripts/dynamicTable.js?locale=${LANG}&v=${CACHEID}"></script> |
||||
<script src="scripts/rename-files.js?v=${CACHEID}"></script> |
||||
<script> |
||||
'use strict'; |
||||
|
||||
if (window.parent.qBittorrent !== undefined) { |
||||
window.qBittorrent = window.parent.qBittorrent; |
||||
} |
||||
window.qBittorrent = window.parent.qBittorrent; |
||||
|
||||
var TriState = window.qBittorrent.FileTree.TriState; |
||||
var data = window.MUI.Windows.instances['multiRenamePage'].options.data; |
||||
var bulkRenameFilesContextMenu; |
||||
if (!bulkRenameFilesContextMenu) { |
||||
bulkRenameFilesContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({ |
||||
targets: '#bulkRenameFilesTableDiv tr', |
||||
menu: 'multiRenameFilesMenu', |
||||
actions: { |
||||
ToggleSelection: function(element, ref) { |
||||
const rowId = parseInt(element.get('data-row-id')); |
||||
const row = bulkRenameFilesTable.getNode(rowId); |
||||
const checkState = row.checked == 1 ? 0 : 1; |
||||
bulkRenameFilesTable.toggleNodeTreeCheckbox(rowId, checkState); |
||||
bulkRenameFilesTable.updateGlobalCheckbox(); |
||||
bulkRenameFilesTable.onRowSelectionChange(bulkRenameFilesTable.getSelectedRows()); |
||||
} |
||||
}, |
||||
offsets: { |
||||
x: -15, |
||||
y: 2 |
||||
} |
||||
}); |
||||
} |
||||
|
||||
// Setup the dynamic table for bulk renaming |
||||
var bulkRenameFilesTable = new window.qBittorrent.DynamicTable.BulkRenameTorrentFilesTable(); |
||||
bulkRenameFilesTable.setup('bulkRenameFilesTableDiv', 'bulkRenameFilesTableFixedHeaderDiv', bulkRenameFilesContextMenu); |
||||
|
||||
// Inject checkbox into the first column of the table header |
||||
var tableHeaders = $$('#bulkRenameFilesTableFixedHeaderDiv .dynamicTableHeader th'); |
||||
var checkboxHeader; |
||||
if (tableHeaders.length > 0) { |
||||
if (checkboxHeader) { |
||||
checkboxHeader.remove(); |
||||
} |
||||
checkboxHeader = new Element('input'); |
||||
checkboxHeader.set('type', 'checkbox'); |
||||
checkboxHeader.set('id', 'rootMultiRename_cb'); |
||||
checkboxHeader.addEvent('click', function(e) { |
||||
bulkRenameFilesTable.toggleGlobalCheckbox(); |
||||
fileRenamer.selectedFiles = bulkRenameFilesTable.getSelectedRows(); |
||||
fileRenamer.update(); |
||||
}); |
||||
|
||||
const checkboxTH = tableHeaders[0]; |
||||
checkboxHeader.injectInside(checkboxTH); |
||||
} |
||||
|
||||
// Register keyboard events to modal window |
||||
if (!keyboard) { |
||||
var keyboard = new Keyboard({ |
||||
defaultEventType: 'keydown', |
||||
events: { |
||||
'Escape': function(event) { |
||||
window.parent.closeWindows(); |
||||
event.preventDefault(); |
||||
}, |
||||
'Esc': function(event) { |
||||
window.parent.closeWindows(); |
||||
event.preventDefault(); |
||||
} |
||||
} |
||||
}); |
||||
keyboard.activate(); |
||||
} |
||||
|
||||
var fileRenamer = new window.qBittorrent.MultiRename.RenameFiles(); |
||||
fileRenamer.hash = data.hash; |
||||
|
||||
// Load Multi Rename Preferences |
||||
var multiRenamePrefChecked = LocalPreferences.get('multirename_rememberPreferences', "true") === "true"; |
||||
$('multirename_rememberprefs_checkbox').setProperty('checked', multiRenamePrefChecked); |
||||
|
||||
if (multiRenamePrefChecked) { |
||||
var multirename_search = LocalPreferences.get('multirename_search', ''); |
||||
fileRenamer.setSearch(multirename_search); |
||||
$('multiRenameSearch').set('value', multirename_search); |
||||
|
||||
var multirename_useRegex = LocalPreferences.get('multirename_useRegex', false); |
||||
fileRenamer.useRegex = multirename_useRegex === 'true'; |
||||
$('use_regex_search').checked = fileRenamer.useRegex; |
||||
|
||||
var multirename_matchAllOccurences = LocalPreferences.get('multirename_matchAllOccurences', false); |
||||
fileRenamer.matchAllOccurences = multirename_matchAllOccurences === 'true'; |
||||
$('match_all_occurences').checked = fileRenamer.matchAllOccurences; |
||||
|
||||
var multirename_caseSensitive = LocalPreferences.get('multirename_caseSensitive', false); |
||||
fileRenamer.caseSensitive = multirename_caseSensitive === 'true'; |
||||
$('case_sensitive').checked = fileRenamer.caseSensitive; |
||||
|
||||
var multirename_replace = LocalPreferences.get('multirename_replace', ''); |
||||
fileRenamer.setReplacement(multirename_replace); |
||||
$('multiRenameReplace').set('value', multirename_replace); |
||||
|
||||
var multirename_appliesTo = LocalPreferences.get('multirename_appliesTo', window.qBittorrent.MultiRename.AppliesTo.FilenameExtension); |
||||
fileRenamer.appliesTo = window.qBittorrent.MultiRename.AppliesTo[multirename_appliesTo]; |
||||
$('applies_to_option').set('value', fileRenamer.appliesTo); |
||||
|
||||
var multirename_includeFiles = LocalPreferences.get('multirename_includeFiles', true); |
||||
fileRenamer.includeFiles = multirename_includeFiles === 'true'; |
||||
$('include_files').checked = fileRenamer.includeFiles; |
||||
|
||||
var multirename_includeFolders = LocalPreferences.get('multirename_includeFolders', false); |
||||
fileRenamer.includeFolders = multirename_includeFolders === 'true'; |
||||
$('include_folders').checked = fileRenamer.includeFolders; |
||||
|
||||
var multirename_fileEnumerationStart = LocalPreferences.get('multirename_fileEnumerationStart', 0); |
||||
fileRenamer.fileEnumerationStart = parseInt(multirename_fileEnumerationStart); |
||||
$('file_counter').set('value', fileRenamer.fileEnumerationStart); |
||||
|
||||
var multirename_replaceAll = LocalPreferences.get('multirename_replaceAll', false); |
||||
fileRenamer.replaceAll = multirename_replaceAll === 'true'; |
||||
var renameButtonValue = fileRenamer.replaceAll ? 'Replace All' : 'Replace'; |
||||
$('renameOptions').set('value', renameButtonValue); |
||||
$('renameButton').set('value', renameButtonValue); |
||||
} |
||||
|
||||
// Fires everytime a row's selection changes |
||||
bulkRenameFilesTable.onRowSelectionChange = function(row) { |
||||
fileRenamer.selectedFiles = bulkRenameFilesTable.getSelectedRows(); |
||||
fileRenamer.update(); |
||||
}; |
||||
|
||||
// Setup Search Events that control renaming |
||||
$('multiRenameSearch').addEvent('input', function(e) { |
||||
let sanitized = e.target.value.replace(/\n/g, ''); |
||||
$('multiRenameSearch').set('value', sanitized); |
||||
|
||||
// Search input has changed |
||||
$('multiRenameSearch').style['border-color'] = ''; |
||||
LocalPreferences.set('multirename_search', sanitized); |
||||
fileRenamer.setSearch(sanitized); |
||||
}); |
||||
$('use_regex_search').addEvent('change', function(e) { |
||||
fileRenamer.useRegex = e.target.checked; |
||||
LocalPreferences.set('multirename_useRegex', e.target.checked); |
||||
fileRenamer.update(); |
||||
}); |
||||
$('match_all_occurences').addEvent('change', function(e) { |
||||
fileRenamer.matchAllOccurences = e.target.checked; |
||||
LocalPreferences.set('multirename_matchAllOccurences', e.target.checked); |
||||
fileRenamer.update(); |
||||
}); |
||||
$('case_sensitive').addEvent('change', function(e) { |
||||
fileRenamer.caseSensitive = e.target.checked; |
||||
LocalPreferences.set('multirename_caseSensitive', e.target.checked); |
||||
fileRenamer.update(); |
||||
}); |
||||
|
||||
/** |
||||
* Fires every time the filerenamer gets changed, it will update all the rows in the table |
||||
*/ |
||||
fileRenamer.onChanged = function(matchedRows) { |
||||
// Clear renamed column |
||||
document |
||||
.querySelectorAll("span[id^='filesTablefileRenamed']") |
||||
.forEach(function(span) { |
||||
span.set('text', ""); |
||||
}); |
||||
|
||||
// Update renamed column for matched rows |
||||
for (let i = 0; i < matchedRows.length; ++i) { |
||||
const row = matchedRows[i]; |
||||
$('filesTablefileRenamed' + row.rowId).set('text', row.renamed); |
||||
} |
||||
}; |
||||
fileRenamer.onInvalidRegex = function(err) { |
||||
$('multiRenameSearch').style['border-color'] = '#CC0033'; |
||||
}; |
||||
|
||||
// Setup Replace Events that control renaming |
||||
$('multiRenameReplace').addEvent('input', function(e) { |
||||
let sanitized = e.target.value.replace(/\n/g, ''); |
||||
$('multiRenameReplace').set('value', sanitized); |
||||
|
||||
// Replace input has changed |
||||
$('multiRenameReplace').style['border-color'] = ''; |
||||
LocalPreferences.set('multirename_replace', sanitized); |
||||
fileRenamer.setReplacement(sanitized); |
||||
}); |
||||
$('applies_to_option').addEvent('change', function(e) { |
||||
fileRenamer.appliesTo = e.target.value; |
||||
LocalPreferences.set('multirename_appliesTo', e.target.value); |
||||
fileRenamer.update(); |
||||
}); |
||||
$('include_files').addEvent('change', function(e) { |
||||
fileRenamer.includeFiles = e.target.checked; |
||||
LocalPreferences.set('multirename_includeFiles', e.target.checked); |
||||
fileRenamer.update(); |
||||
}); |
||||
$('include_folders').addEvent('change', function(e) { |
||||
fileRenamer.includeFolders = e.target.checked; |
||||
LocalPreferences.set('multirename_includeFolders', e.target.checked); |
||||
fileRenamer.update(); |
||||
}); |
||||
$('file_counter').addEvent('input', function(e) { |
||||
let value = e.target.valueAsNumber; |
||||
if (!value) { value = 0; } |
||||
if (value < 0) { value = 0; } |
||||
if (value > 99999999) { value = 99999999; } |
||||
fileRenamer.fileEnumerationStart = value; |
||||
$('file_counter').set('value', value); |
||||
LocalPreferences.set('multirename_fileEnumerationStart', value); |
||||
fileRenamer.update(); |
||||
}); |
||||
|
||||
// Setup Rename Operation Events |
||||
$('renameButton').addEvent('click', function(e) { |
||||
// Disable Search Options |
||||
$('multiRenameSearch').disabled = true; |
||||
$('use_regex_search').disabled = true; |
||||
$('match_all_occurences').disabled = true; |
||||
$('case_sensitive').disabled = true; |
||||
// Disable Replace Options |
||||
$('multiRenameReplace').disabled = true; |
||||
$('applies_to_option').disabled = true; |
||||
$('include_files').disabled = true; |
||||
$('include_folders').disabled = true; |
||||
$('file_counter').disabled = true; |
||||
// Disable Rename Buttons |
||||
$('renameButton').disabled = true; |
||||
$('renameOptions').disabled = true; |
||||
// Clear error text |
||||
$('rename_error').set('text', ''); |
||||
fileRenamer.rename(); |
||||
}); |
||||
fileRenamer.onRenamed = function(rows) { |
||||
// Disable Search Options |
||||
$('multiRenameSearch').disabled = false; |
||||
$('use_regex_search').disabled = false; |
||||
$('match_all_occurences').disabled = false; |
||||
$('case_sensitive').disabled = false; |
||||
// Disable Replace Options |
||||
$('multiRenameReplace').disabled = false; |
||||
$('applies_to_option').disabled = false; |
||||
$('include_files').disabled = false; |
||||
$('include_folders').disabled = false; |
||||
$('file_counter').disabled = false; |
||||
// Disable Rename Buttons |
||||
$('renameButton').disabled = false; |
||||
$('renameOptions').disabled = false; |
||||
|
||||
// Recreate table |
||||
let selectedRows = bulkRenameFilesTable.getSelectedRows().map(row => row.rowId.toString()); |
||||
for (let renamedRow of rows) { |
||||
selectedRows = selectedRows.filter(selectedRow => selectedRow !== renamedRow.rowId.toString()); |
||||
} |
||||
bulkRenameFilesTable.clear(); |
||||
|
||||
// Adjust file enumeration count by 1 when replacing single files to prevent naming conflicts |
||||
if (!fileRenamer.replaceAll) { |
||||
fileRenamer.fileEnumerationStart++; |
||||
$('file_counter').set('value', fileRenamer.fileEnumerationStart); |
||||
} |
||||
setupTable(selectedRows); |
||||
}; |
||||
fileRenamer.onRenameError = function(err, row) { |
||||
if (err.xhr.status === 409) { |
||||
$('rename_error').set('text', `QBT_TR(Rename failed: file or folder already exists)QBT_TR[CONTEXT=PropertiesWidget] \`${row.renamed}\``); |
||||
} |
||||
}; |
||||
$('renameOptions').addEvent('change', function(e) { |
||||
const combobox = e.target; |
||||
const replaceOperation = combobox.value; |
||||
if (replaceOperation == "Replace") { |
||||
fileRenamer.replaceAll = false; |
||||
} |
||||
else if (replaceOperation == "Replace All") { |
||||
fileRenamer.replaceAll = true; |
||||
} |
||||
else { |
||||
fileRenamer.replaceAll = false; |
||||
} |
||||
LocalPreferences.set('multirename_replaceAll', fileRenamer.replaceAll); |
||||
$('renameButton').set('value', replaceOperation); |
||||
}); |
||||
$('closeButton').addEvent('click', function() { |
||||
window.parent.closeWindows(); |
||||
event.preventDefault(); |
||||
}); |
||||
// synchronize header scrolling to table body |
||||
$('bulkRenameFilesTableDiv').onscroll = function() { |
||||
const length = $(this).scrollLeft; |
||||
$('bulkRenameFilesTableFixedHeaderDiv').scrollLeft = length; |
||||
}; |
||||
|
||||
var handleTorrentFiles = function(files, selectedRows) { |
||||
const rows = files.map(function(file, index) { |
||||
|
||||
const row = { |
||||
fileId: index, |
||||
checked: 1, // unchecked |
||||
path: file.name, |
||||
original: window.qBittorrent.Filesystem.fileName(file.name), |
||||
renamed: "", |
||||
size: file.size |
||||
}; |
||||
|
||||
return row; |
||||
}); |
||||
|
||||
addRowsToTable(rows, selectedRows); |
||||
}; |
||||
|
||||
var addRowsToTable = function(rows, selectedRows) { |
||||
let rowId = 0; |
||||
const rootNode = new window.qBittorrent.FileTree.FolderNode(); |
||||
rootNode.autoCheckFolders = false; |
||||
|
||||
rows.forEach(function(row) { |
||||
const pathItems = row.path.split(window.qBittorrent.Filesystem.PathSeparator); |
||||
|
||||
pathItems.pop(); // remove last item (i.e. file name) |
||||
let parent = rootNode; |
||||
pathItems.forEach(function(folderName) { |
||||
if (folderName === '.unwanted') { |
||||
return; |
||||
} |
||||
|
||||
let folderNode = null; |
||||
if (parent.children !== null) { |
||||
for (let i = 0; i < parent.children.length; ++i) { |
||||
const childFolder = parent.children[i]; |
||||
if (childFolder.original === folderName) { |
||||
folderNode = childFolder; |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (folderNode === null) { |
||||
folderNode = new window.qBittorrent.FileTree.FolderNode(); |
||||
folderNode.autoCheckFolders = false; |
||||
folderNode.rowId = rowId; |
||||
folderNode.path = (parent.path === "") |
||||
? folderName |
||||
: [parent.path, folderName].join(window.qBittorrent.Filesystem.PathSeparator); |
||||
folderNode.checked = selectedRows.includes(rowId.toString()) ? 0 : 1; |
||||
folderNode.original = folderName; |
||||
folderNode.renamed = ""; |
||||
folderNode.root = parent; |
||||
parent.addChild(folderNode); |
||||
|
||||
++rowId; |
||||
} |
||||
|
||||
parent = folderNode; |
||||
}); |
||||
|
||||
const childNode = new window.qBittorrent.FileTree.FileNode(); |
||||
childNode.rowId = rowId; |
||||
childNode.path = row.path; |
||||
childNode.checked = selectedRows.includes(rowId.toString()) ? 0 : 1; |
||||
childNode.original = row.original; |
||||
childNode.renamed = ""; |
||||
childNode.root = parent; |
||||
childNode.data = row; |
||||
parent.addChild(childNode); |
||||
|
||||
++rowId; |
||||
}); |
||||
|
||||
bulkRenameFilesTable.populateTable(rootNode); |
||||
bulkRenameFilesTable.updateTable(false); |
||||
bulkRenameFilesTable.altRow(); |
||||
|
||||
if (selectedRows !== undefined) { |
||||
bulkRenameFilesTable.reselectRows(selectedRows); |
||||
} |
||||
fileRenamer.selectedFiles = bulkRenameFilesTable.getSelectedRows(); |
||||
fileRenamer.update(); |
||||
}; |
||||
|
||||
var setupTable = function(selectedRows) { |
||||
new Request.JSON({ |
||||
url: new URI('api/v2/torrents/files?hash=' + data.hash), |
||||
noCache: true, |
||||
method: 'get', |
||||
onSuccess: function(files) { |
||||
if (files.length === 0) { |
||||
bulkRenameFilesTable.clear(); |
||||
} |
||||
else { |
||||
handleTorrentFiles(files, selectedRows); |
||||
} |
||||
} |
||||
}).send(); |
||||
}; |
||||
setupTable(data.selectedRows); |
||||
</script> |
||||
</head> |
||||
|
||||
<body style="min-width: 400px; min-height: 300px;"> |
||||
<div style="padding: 0px 10px 0px 0px;"> |
||||
<div style="float: left; height: 100%; width: 228px;"> |
||||
<div class="formRow"> |
||||
<input type="checkbox" id="multirename_rememberprefs_checkbox" onchange="LocalPreferences.set('multirename_rememberPreferences', this.checked);" /> |
||||
<label for="multirename_rememberprefs_checkbox">QBT_TR(Remember Multi-Rename settings)QBT_TR[CONTEXT=OptionsDialog]</label> |
||||
</div> |
||||
<hr> |
||||
<textarea id="multiRenameSearch" placeholder="QBT_TR(Search Files)QBT_TR[CONTEXT=PropertiesWidget]" style="width: calc(100% - 8px); resize: vertical; min-height: 30px;"></textarea> |
||||
<div class="formRow"> |
||||
<input type="checkbox" id="use_regex_search" /> |
||||
<label for="use_regex_search">QBT_TR(Use regular expressions)QBT_TR[CONTEXT=PropertiesWidget]</label> |
||||
</div> |
||||
<div class="formRow"> |
||||
<input type="checkbox" id="match_all_occurences" /> |
||||
<label for="match_all_occurences">QBT_TR(Match all occurences)QBT_TR[CONTEXT=PropertiesWidget]</label> |
||||
</div> |
||||
<div class="formRow"> |
||||
<input type="checkbox" id="case_sensitive" /> |
||||
<label for="case_sensitive">QBT_TR(Case sensitive)QBT_TR[CONTEXT=PropertiesWidget]</label> |
||||
</div> |
||||
<hr> |
||||
<textarea id="multiRenameReplace" placeholder="QBT_TR(Replacement Input)QBT_TR[CONTEXT=PropertiesWidget]" style="width: calc(100% - 8px); resize: vertical; min-height: 30px;"></textarea> |
||||
<select id="applies_to_option" name="applies_to_option" style="width: 100%; margin-bottom: 5px;"> |
||||
<option selected value="FilenameExtension">QBT_TR(Filename + Extension)QBT_TR[CONTEXT=PropertiesWidget]</option> |
||||
<option value="Filename">QBT_TR(Filename)QBT_TR[CONTEXT=PropertiesWidget]</option> |
||||
<option value="Extension">QBT_TR(Extension)QBT_TR[CONTEXT=PropertiesWidget]</option> |
||||
</select> |
||||
<div class="formRow"> |
||||
<input type="checkbox" id="include_files" checked /> |
||||
<label for="include_files">QBT_TR(Include files)QBT_TR[CONTEXT=PropertiesWidget]</label> |
||||
</div> |
||||
<div class="formRow"> |
||||
<input type="checkbox" id="include_folders" /> |
||||
<label for="include_folders">QBT_TR(Include folders)QBT_TR[CONTEXT=PropertiesWidget]</label> |
||||
</div> |
||||
<div class="formRow"> |
||||
<input type="number" min="0" max="99999999" value="0" id="file_counter" style="width: 80px;" /> |
||||
<label for="file_counter">QBT_TR(Enumerate Files)QBT_TR[CONTEXT=PropertiesWidget]</label> |
||||
</div> |
||||
</div> |
||||
<div id="operation_btns" style="position: absolute; left: 0; bottom: 0; margin: 0px 12px 36px 12px; width: 228px;background: #ffffff;padding: 0px 5px 10px 0px;"> |
||||
<div style="overflow: auto;"> |
||||
<span id="rename_error" style="float: unset; font-size: unset;"></span> |
||||
</div> |
||||
<hr> |
||||
<div style="width: 60%; float: left;"> |
||||
<input id="renameButton" type="button" value="Replace" style="float: left; width: 86px;"> |
||||
<select id="renameOptions" name="renameOptions" style="width: 22px;"> |
||||
<option selected value="Replace">QBT_TR(Replace)QBT_TR[CONTEXT=PropertiesWidget]</option> |
||||
<option value="Replace All">QBT_TR(Replace All)QBT_TR[CONTEXT=PropertiesWidget]</option> |
||||
</select> |
||||
</div> |
||||
<input id="closeButton" type="button" value="Close" style="float: right; width: 30%;"> |
||||
</div> |
||||
<div id="torrentFiles" class="panel" style="position: absolute; top: 0; right: 0; bottom: 0; left: 228px; margin: 35px 10px 45px 20px; border-bottom: 0"> |
||||
<div id="bulkRenameFilesTableFixedHeaderDiv" class="dynamicTableFixedHeaderDiv"> |
||||
<table class="dynamicTable"> |
||||
<thead> |
||||
<tr class="dynamicTableHeader"></tr> |
||||
</thead> |
||||
</table> |
||||
</div> |
||||
<div id="bulkRenameFilesTableDiv" class="dynamicTableDiv"> |
||||
<table class="dynamicTable"> |
||||
<thead> |
||||
<tr class="dynamicTableHeader"></tr> |
||||
</thead> |
||||
<tbody></tbody> |
||||
</table> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</body> |
||||
|
||||
</html> |
@ -0,0 +1,286 @@
@@ -0,0 +1,286 @@
|
||||
'use strict'; |
||||
|
||||
if (window.qBittorrent === undefined) { |
||||
window.qBittorrent = {}; |
||||
} |
||||
|
||||
window.qBittorrent.MultiRename = (function() { |
||||
const exports = function() { |
||||
return { |
||||
AppliesTo: AppliesTo, |
||||
RenameFiles: RenameFiles |
||||
}; |
||||
}; |
||||
|
||||
const AppliesTo = { |
||||
"FilenameExtension": "FilenameExtension", |
||||
"Filename": "Filename", |
||||
"Extension": "Extension" |
||||
}; |
||||
|
||||
const RenameFiles = new Class({ |
||||
hash: '', |
||||
selectedFiles: [], |
||||
matchedFiles: [], |
||||
|
||||
// Search Options
|
||||
_inner_search: "", |
||||
setSearch(val) { |
||||
this._inner_search = val; |
||||
this._inner_update(); |
||||
this.onChanged(this.matchedFiles); |
||||
}, |
||||
useRegex: false, |
||||
matchAllOccurences: false, |
||||
caseSensitive: false, |
||||
|
||||
// Replacement Options
|
||||
_inner_replacement: "", |
||||
setReplacement(val) { |
||||
this._inner_replacement = val; |
||||
this._inner_update(); |
||||
this.onChanged(this.matchedFiles); |
||||
}, |
||||
appliesTo: AppliesTo.FilenameExtension, |
||||
includeFiles: true, |
||||
includeFolders: false, |
||||
replaceAll: false, |
||||
fileEnumerationStart: 0, |
||||
|
||||
onChanged: function(rows) {}, |
||||
onInvalidRegex: function(err) {}, |
||||
onRenamed: function(rows) {}, |
||||
onRenameError: function(err) {}, |
||||
|
||||
_inner_update: function() { |
||||
const findMatches = (regex, str) => { |
||||
let result; |
||||
let count = 0; |
||||
let lastIndex = 0; |
||||
regex.lastIndex = 0; |
||||
let matches = []; |
||||
do { |
||||
result = regex.exec(str); |
||||
|
||||
if (result == null) { break; } |
||||
matches.push(result); |
||||
|
||||
// regex assertions don't modify lastIndex,
|
||||
// so we need to explicitly break out to prevent infinite loop
|
||||
if (lastIndex == regex.lastIndex) { |
||||
break; |
||||
} |
||||
else { |
||||
lastIndex = regex.lastIndex; |
||||
} |
||||
|
||||
// Maximum of 250 matches per file
|
||||
++count; |
||||
} while (regex.global && count < 250); |
||||
|
||||
return matches; |
||||
}; |
||||
|
||||
const replaceBetween = (input, start, end, replacement) => { |
||||
return input.substring(0, start) + replacement + input.substring(end); |
||||
}; |
||||
const replaceGroup = (input, search, replacement, escape, stripEscape = true) => { |
||||
let result = ''; |
||||
let i = 0; |
||||
while (i < input.length) { |
||||
// Check if the current index contains the escape string
|
||||
if (input.substring(i, i + escape.length) === escape) { |
||||
// Don't replace escape chars when they don't precede the current search being performed
|
||||
if (input.substring(i + escape.length, i + escape.length + search.length) !== search) { |
||||
result += input[i]; |
||||
i++; |
||||
continue; |
||||
} |
||||
// Replace escape chars when they precede the current search being performed, unless explicitly told not to
|
||||
if (stripEscape) { |
||||
result += input.substring(i + escape.length, i + escape.length + search.length); |
||||
i += escape.length + search.length; |
||||
} |
||||
else { |
||||
result += input.substring(i, i + escape.length + search.length); |
||||
i += escape.length + search.length; |
||||
} |
||||
// Check if the current index contains the search string
|
||||
} |
||||
else if (input.substring(i, i + search.length) === search) { |
||||
result += replacement; |
||||
i += search.length; |
||||
// Append characters that didn't meet the previous critera
|
||||
} |
||||
else { |
||||
result += input[i]; |
||||
i++; |
||||
} |
||||
} |
||||
return result; |
||||
}; |
||||
|
||||
this.matchedFiles = []; |
||||
|
||||
// Ignore empty searches
|
||||
if (!this._inner_search) { |
||||
return; |
||||
} |
||||
|
||||
// Setup regex flags
|
||||
let regexFlags = ""; |
||||
if (this.matchAllOccurences) { regexFlags += "g"; } |
||||
if (!this.caseSensitive) { regexFlags += "i"; } |
||||
|
||||
// Setup regex search
|
||||
const regexEscapeExp = new RegExp(/[/\-\\^$*+?.()|[\]{}]/g); |
||||
const standardSearch = new RegExp(this._inner_search.replace(regexEscapeExp, '\\$&'), regexFlags); |
||||
let regexSearch; |
||||
try { |
||||
regexSearch = new RegExp(this._inner_search, regexFlags); |
||||
} |
||||
catch (err) { |
||||
if (this.useRegex) { |
||||
this.onInvalidRegex(err); |
||||
return; |
||||
} |
||||
} |
||||
const search = this.useRegex ? regexSearch : standardSearch; |
||||
|
||||
let fileEnumeration = this.fileEnumerationStart; |
||||
for (let i = 0; i < this.selectedFiles.length; ++i) { |
||||
const row = this.selectedFiles[i]; |
||||
|
||||
// Ignore files
|
||||
if (!row.isFolder && !this.includeFiles) { |
||||
continue; |
||||
} |
||||
// Ignore folders
|
||||
else if (row.isFolder && !this.includeFolders) { |
||||
continue; |
||||
} |
||||
|
||||
// Get file extension and reappend the "." (only when the file has an extension)
|
||||
let fileExtension = window.qBittorrent.Filesystem.fileExtension(row.original); |
||||
if (fileExtension) { fileExtension = "." + fileExtension; } |
||||
|
||||
const fileNameWithoutExt = row.original.slice(0, row.original.lastIndexOf(fileExtension)); |
||||
|
||||
let matches = []; |
||||
let offset = 0; |
||||
switch (this.appliesTo) { |
||||
case "FilenameExtension": |
||||
matches = findMatches(search, `${fileNameWithoutExt}${fileExtension}`); |
||||
break; |
||||
case "Filename": |
||||
matches = findMatches(search, `${fileNameWithoutExt}`); |
||||
break; |
||||
case "Extension": |
||||
// Adjust the offset to ensure we perform the replacement at the extension location
|
||||
offset = fileNameWithoutExt.length; |
||||
matches = findMatches(search, `${fileExtension}`); |
||||
break; |
||||
} |
||||
// Ignore rows without a match
|
||||
if (!matches || matches.length == 0) { |
||||
continue; |
||||
} |
||||
|
||||
let renamed = row.original; |
||||
for (let i = matches.length - 1; i >= 0; --i) { |
||||
const match = matches[i]; |
||||
let replacement = this._inner_replacement; |
||||
// Replace numerical groups
|
||||
for (let g = 0; g < match.length; ++g) { |
||||
let group = match[g]; |
||||
if (!group) { continue; } |
||||
replacement = replaceGroup(replacement, `$${g}`, group, '\\', false); |
||||
} |
||||
// Replace named groups
|
||||
for (let namedGroup in match.groups) { |
||||
replacement = replaceGroup(replacement, `$${namedGroup}`, match.groups[namedGroup], '\\', false); |
||||
} |
||||
// Replace auxillary variables
|
||||
for (let v = 'dddddddd'; v !== ''; v = v.substring(1)) { |
||||
let fileCount = fileEnumeration.toString().padStart(v.length, '0'); |
||||
replacement = replaceGroup(replacement, `$${v}`, fileCount, '\\', false); |
||||
} |
||||
// Remove empty $ variable
|
||||
replacement = replaceGroup(replacement, '$', '', '\\'); |
||||
const wholeMatch = match[0]; |
||||
const index = match['index']; |
||||
renamed = replaceBetween(renamed, index + offset, index + offset + wholeMatch.length, replacement); |
||||
} |
||||
|
||||
row.renamed = renamed; |
||||
++fileEnumeration; |
||||
this.matchedFiles.push(row); |
||||
} |
||||
}, |
||||
|
||||
rename: async function() { |
||||
if (!this.matchedFiles || this.matchedFiles.length === 0 || !this.hash) { |
||||
this.onRenamed([]); |
||||
return; |
||||
} |
||||
|
||||
let replaced = []; |
||||
const _inner_rename = async function(i) { |
||||
const match = this.matchedFiles[i]; |
||||
const newName = match.renamed; |
||||
if (newName === match.original) { |
||||
// Original file name is identical to Renamed
|
||||
return; |
||||
} |
||||
|
||||
const isFolder = match.isFolder; |
||||
const parentPath = window.qBittorrent.Filesystem.folderName(match.path); |
||||
const oldPath = parentPath |
||||
? parentPath + window.qBittorrent.Filesystem.PathSeparator + match.original |
||||
: match.original; |
||||
const newPath = parentPath |
||||
? parentPath + window.qBittorrent.Filesystem.PathSeparator + newName |
||||
: newName; |
||||
let renameRequest = new Request({ |
||||
url: isFolder ? 'api/v2/torrents/renameFolder' : 'api/v2/torrents/renameFile', |
||||
method: 'post', |
||||
data: { |
||||
hash: this.hash, |
||||
oldPath: oldPath, |
||||
newPath: newPath |
||||
} |
||||
}); |
||||
try { |
||||
await renameRequest.send(); |
||||
replaced.push(match); |
||||
} |
||||
catch (err) { |
||||
this.onRenameError(err, match); |
||||
} |
||||
}.bind(this); |
||||
|
||||
const replacements = this.matchedFiles.length; |
||||
if (this.replaceAll) { |
||||
// matchedFiles are in DFS order so we rename in reverse
|
||||
// in order to prevent unwanted folder creation
|
||||
for (let i = replacements - 1; i >= 0; --i) { |
||||
await _inner_rename(i); |
||||
} |
||||
} |
||||
else { |
||||
// single replacements go linearly top-down because the
|
||||
// file tree gets recreated after every rename
|
||||
await _inner_rename(0); |
||||
} |
||||
this.onRenamed(replaced); |
||||
}, |
||||
update: function() { |
||||
this._inner_update(); |
||||
this.onChanged(this.matchedFiles); |
||||
} |
||||
}); |
||||
|
||||
return exports(); |
||||
})(); |
||||
|
||||
Object.freeze(window.qBittorrent.MultiRename); |
Loading…
Reference in new issue