3 if (window
.qBittorrent
=== undefined) {
4 window
.qBittorrent
= {};
7 window
.qBittorrent
.MultiRename
= (function() {
8 const exports = function() {
11 RenameFiles
: RenameFiles
16 "FilenameExtension": "FilenameExtension",
17 "Filename": "Filename",
18 "Extension": "Extension"
21 const RenameFiles
= new Class({
29 this._inner_search
= val
;
31 this.onChanged(this.matchedFiles
);
34 matchAllOccurrences
: false,
37 // Replacement Options
38 _inner_replacement
: "",
40 this._inner_replacement
= val
;
42 this.onChanged(this.matchedFiles
);
44 appliesTo
: AppliesTo
.FilenameExtension
,
46 includeFolders
: false,
48 fileEnumerationStart
: 0,
50 onChanged: function(rows
) {},
51 onInvalidRegex: function(err
) {},
52 onRenamed: function(rows
) {},
53 onRenameError: function(err
) {},
55 _inner_update: function() {
56 const findMatches
= (regex
, str
) => {
63 result
= regex
.exec(str
);
65 if (result
== null) { break; }
68 // regex assertions don't modify lastIndex,
69 // so we need to explicitly break out to prevent infinite loop
70 if (lastIndex
== regex
.lastIndex
) {
74 lastIndex
= regex
.lastIndex
;
77 // Maximum of 250 matches per file
79 } while (regex
.global
&& count
< 250);
84 const replaceBetween
= (input
, start
, end
, replacement
) => {
85 return input
.substring(0, start
) + replacement
+ input
.substring(end
);
87 const replaceGroup
= (input
, search
, replacement
, escape
, stripEscape
= true) => {
90 while (i
< input
.length
) {
91 // Check if the current index contains the escape string
92 if (input
.substring(i
, i
+ escape
.length
) === escape
) {
93 // Don't replace escape chars when they don't precede the current search being performed
94 if (input
.substring(i
+ escape
.length
, i
+ escape
.length
+ search
.length
) !== search
) {
99 // Replace escape chars when they precede the current search being performed, unless explicitly told not to
101 result
+= input
.substring(i
+ escape
.length
, i
+ escape
.length
+ search
.length
);
102 i
+= escape
.length
+ search
.length
;
105 result
+= input
.substring(i
, i
+ escape
.length
+ search
.length
);
106 i
+= escape
.length
+ search
.length
;
108 // Check if the current index contains the search string
110 else if (input
.substring(i
, i
+ search
.length
) === search
) {
111 result
+= replacement
;
113 // Append characters that didn't meet the previous criteria
123 this.matchedFiles
= [];
125 // Ignore empty searches
126 if (!this._inner_search
) {
132 if (this.matchAllOccurrences
) { regexFlags
+= "g"; }
133 if (!this.caseSensitive
) { regexFlags
+= "i"; }
135 // Setup regex search
136 const regexEscapeExp
= new RegExp(/[/\-\\^$*+?.()|[\]{}]/g);
137 const standardSearch
= new RegExp(this._inner_search
.replace(regexEscapeExp
, '\\$&'), regexFlags
);
140 regexSearch
= new RegExp(this._inner_search
, regexFlags
);
144 this.onInvalidRegex(err
);
148 const search
= this.useRegex
? regexSearch
: standardSearch
;
150 let fileEnumeration
= this.fileEnumerationStart
;
151 for (let i
= 0; i
< this.selectedFiles
.length
; ++i
) {
152 const row
= this.selectedFiles
[i
];
155 if (!row
.isFolder
&& !this.includeFiles
) {
159 else if (row
.isFolder
&& !this.includeFolders
) {
163 // Get file extension and reappend the "." (only when the file has an extension)
164 let fileExtension
= window
.qBittorrent
.Filesystem
.fileExtension(row
.original
);
165 if (fileExtension
) { fileExtension
= "." + fileExtension
; }
167 const fileNameWithoutExt
= row
.original
.slice(0, row
.original
.lastIndexOf(fileExtension
));
171 switch (this.appliesTo
) {
172 case "FilenameExtension":
173 matches
= findMatches(search
, `${fileNameWithoutExt}${fileExtension}`);
176 matches
= findMatches(search
, `${fileNameWithoutExt}`);
179 // Adjust the offset to ensure we perform the replacement at the extension location
180 offset
= fileNameWithoutExt
.length
;
181 matches
= findMatches(search
, `${fileExtension}`);
184 // Ignore rows without a match
185 if (!matches
|| matches
.length
== 0) {
189 let renamed
= row
.original
;
190 for (let i
= matches
.length
- 1; i
>= 0; --i
) {
191 const match
= matches
[i
];
192 let replacement
= this._inner_replacement
;
193 // Replace numerical groups
194 for (let g
= 0; g
< match
.length
; ++g
) {
195 let group
= match
[g
];
196 if (!group
) { continue; }
197 replacement
= replaceGroup(replacement
, `$${g}`, group
, '\\', false);
199 // Replace named groups
200 for (let namedGroup
in match
.groups
) {
201 replacement
= replaceGroup(replacement
, `$${namedGroup}`, match
.groups
[namedGroup
], '\\', false);
203 // Replace auxiliary variables
204 for (let v
= 'dddddddd'; v
!== ''; v
= v
.substring(1)) {
205 let fileCount
= fileEnumeration
.toString().padStart(v
.length
, '0');
206 replacement
= replaceGroup(replacement
, `$${v}`, fileCount
, '\\', false);
208 // Remove empty $ variable
209 replacement
= replaceGroup(replacement
, '$', '', '\\');
210 const wholeMatch
= match
[0];
211 const index
= match
['index'];
212 renamed
= replaceBetween(renamed
, index
+ offset
, index
+ offset
+ wholeMatch
.length
, replacement
);
215 row
.renamed
= renamed
;
217 this.matchedFiles
.push(row
);
221 rename
: async
function() {
222 if (!this.matchedFiles
|| this.matchedFiles
.length
=== 0 || !this.hash
) {
228 const _inner_rename
= async
function(i
) {
229 const match
= this.matchedFiles
[i
];
230 const newName
= match
.renamed
;
231 if (newName
=== match
.original
) {
232 // Original file name is identical to Renamed
236 const isFolder
= match
.isFolder
;
237 const parentPath
= window
.qBittorrent
.Filesystem
.folderName(match
.path
);
238 const oldPath
= parentPath
239 ? parentPath
+ window
.qBittorrent
.Filesystem
.PathSeparator
+ match
.original
241 const newPath
= parentPath
242 ? parentPath
+ window
.qBittorrent
.Filesystem
.PathSeparator
+ newName
244 let renameRequest
= new Request({
245 url
: isFolder
? 'api/v2/torrents/renameFolder' : 'api/v2/torrents/renameFile',
254 await renameRequest
.send();
255 replaced
.push(match
);
258 this.onRenameError(err
, match
);
262 const replacements
= this.matchedFiles
.length
;
263 if (this.replaceAll
) {
264 // matchedFiles are in DFS order so we rename in reverse
265 // in order to prevent unwanted folder creation
266 for (let i
= replacements
- 1; i
>= 0; --i
) {
267 await
_inner_rename(i
);
271 // single replacements go linearly top-down because the
272 // file tree gets recreated after every rename
273 await
_inner_rename(0);
275 this.onRenamed(replaced
);
278 this._inner_update();
279 this.onChanged(this.matchedFiles
);
286 Object
.freeze(window
.qBittorrent
.MultiRename
);