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
: (rows
) => {},
48 onInvalidRegex
: (err
) => {},
49 onRenamed
: (rows
) => {},
50 onRenameError
: (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
);