3 window.qBittorrent ??= {};
4 window.qBittorrent.MultiRename ??= (() => {
5 const exports = () => {
8 RenameFiles: RenameFiles
13 "FilenameExtension": "FilenameExtension",
14 "Filename": "Filename",
15 "Extension": "Extension"
18 const RenameFiles = new Class({
26 this._inner_search = val;
28 this.onChanged(this.matchedFiles);
31 matchAllOccurrences: false,
34 // Replacement Options
35 _inner_replacement: "",
37 this._inner_replacement = val;
39 this.onChanged(this.matchedFiles);
41 appliesTo: AppliesTo.FilenameExtension,
43 includeFolders: false,
45 fileEnumerationStart: 0,
47 onChanged: function(rows) {},
48 onInvalidRegex: function(err) {},
49 onRenamed: function(rows) {},
50 onRenameError: function(err) {},
52 _inner_update: function() {
53 const findMatches = (regex, str) => {
60 result = regex.exec(str);
66 // regex assertions don't modify lastIndex,
67 // so we need to explicitly break out to prevent infinite loop
68 if (lastIndex === regex.lastIndex)
71 lastIndex = regex.lastIndex;
73 // Maximum of 250 matches per file
75 } while (regex.global && (count < 250));
80 const replaceBetween = (input, start, end, replacement) => {
81 return input.substring(0, start) + replacement + input.substring(end);
83 const replaceGroup = (input, search, replacement, escape, stripEscape = true) => {
86 while (i < input.length) {
87 // Check if the current index contains the escape string
88 if (input.substring(i, i + escape.length) === escape) {
89 // Don't replace escape chars when they don't precede the current search being performed
90 if (input.substring(i + escape.length, i + escape.length + search.length) !== search) {
95 // Replace escape chars when they precede the current search being performed, unless explicitly told not to
97 result += input.substring(i + escape.length, i + escape.length + search.length);
98 i += escape.length + search.length;
101 result += input.substring(i, i + escape.length + search.length);
102 i += escape.length + search.length;
104 // Check if the current index contains the search string
106 else if (input.substring(i, i + search.length) === search) {
107 result += replacement;
109 // Append characters that didn't meet the previous criteria
119 this.matchedFiles = [];
121 // Ignore empty searches
122 if (!this._inner_search)
127 if (this.matchAllOccurrences)
129 if (!this.caseSensitive)
132 // Setup regex search
133 const regexEscapeExp = new RegExp(/[/\-\\^$*+?.()|[\]{}]/g);
134 const standardSearch = new RegExp(this._inner_search.replace(regexEscapeExp, "\\$&"), regexFlags);
137 regexSearch = new RegExp(this._inner_search, regexFlags);
141 this.onInvalidRegex(err);
145 const search = this.useRegex ? regexSearch : standardSearch;
147 let fileEnumeration = this.fileEnumerationStart;
148 for (let i = 0; i < this.selectedFiles.length; ++i) {
149 const row = this.selectedFiles[i];
152 if (!row.isFolder && !this.includeFiles)
156 else if (row.isFolder && !this.includeFolders)
159 // Get file extension and reappend the "." (only when the file has an extension)
160 let fileExtension = window.qBittorrent.Filesystem.fileExtension(row.original);
162 fileExtension = "." + fileExtension;
164 const fileNameWithoutExt = row.original.slice(0, row.original.lastIndexOf(fileExtension));
168 switch (this.appliesTo) {
169 case "FilenameExtension":
170 matches = findMatches(search, `${fileNameWithoutExt}${fileExtension}`);
173 matches = findMatches(search, `${fileNameWithoutExt}`);
176 // Adjust the offset to ensure we perform the replacement at the extension location
177 offset = fileNameWithoutExt.length;
178 matches = findMatches(search, `${fileExtension}`);
181 // Ignore rows without a match
182 if (!matches || (matches.length === 0))
185 let renamed = row.original;
186 for (let i = matches.length - 1; i >= 0; --i) {
187 const match = matches[i];
188 let replacement = this._inner_replacement;
189 // Replace numerical groups
190 for (let g = 0; g < match.length; ++g) {
191 const group = match[g];
194 replacement = replaceGroup(replacement, `$${g}`, group, "\\", false);
196 // Replace named groups
197 for (const namedGroup in match.groups) {
198 if (!Object.hasOwn(match.groups, namedGroup))
200 replacement = replaceGroup(replacement, `$${namedGroup}`, match.groups[namedGroup], "\\", false);
202 // Replace auxiliary variables
203 for (let v = "dddddddd"; v !== ""; v = v.substring(1)) {
204 const fileCount = fileEnumeration.toString().padStart(v.length, "0");
205 replacement = replaceGroup(replacement, `$${v}`, fileCount, "\\", false);
207 // Remove empty $ variable
208 replacement = replaceGroup(replacement, "$", "", "\\");
209 const wholeMatch = match[0];
210 const index = match["index"];
211 renamed = replaceBetween(renamed, index + offset, index + offset + wholeMatch.length, replacement);
214 row.renamed = renamed;
216 this.matchedFiles.push(row);
220 rename: async function() {
221 if (!this.matchedFiles || (this.matchedFiles.length === 0) || !this.hash) {
227 const _inner_rename = async function(i) {
228 const match = this.matchedFiles[i];
229 const newName = match.renamed;
230 if (newName === match.original) {
231 // Original file name is identical to Renamed
235 const isFolder = match.isFolder;
236 const parentPath = window.qBittorrent.Filesystem.folderName(match.path);
237 const oldPath = parentPath
238 ? parentPath + window.qBittorrent.Filesystem.PathSeparator + match.original
240 const newPath = parentPath
241 ? parentPath + window.qBittorrent.Filesystem.PathSeparator + newName
243 const renameRequest = new Request({
244 url: isFolder ? "api/v2/torrents/renameFolder" : "api/v2/torrents/renameFile",
253 await renameRequest.send();
254 replaced.push(match);
257 this.onRenameError(err, match);
261 const replacements = this.matchedFiles.length;
262 if (this.replaceAll) {
263 // matchedFiles are in DFS order so we rename in reverse
264 // in order to prevent unwanted folder creation
265 for (let i = replacements - 1; i >= 0; --i)
266 await _inner_rename(i);
269 // single replacements go linearly top-down because the
270 // file tree gets recreated after every rename
271 await _inner_rename(0);
273 this.onRenamed(replaced);
276 this._inner_update();
277 this.onChanged(this.matchedFiles);
283 Object.freeze(window.qBittorrent.MultiRename);