@mux-magic/tools 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +37 -0
- package/src/__fixtures__/badBooleanName.ts +2 -0
- package/src/aclSafeCopyFile.test.ts +136 -0
- package/src/aclSafeCopyFile.ts +136 -0
- package/src/addFolderNameBeforeFilename.ts +13 -0
- package/src/captureConsoleMessage.test.ts +127 -0
- package/src/captureConsoleMessage.ts +22 -0
- package/src/captureLogMessage.ts +11 -0
- package/src/cleanupFilename.test.ts +26 -0
- package/src/cleanupFilename.ts +14 -0
- package/src/createRenameFileOrFolder.test.ts +220 -0
- package/src/createRenameFileOrFolder.ts +79 -0
- package/src/eslintBooleanPrefixRule.test.ts +30 -0
- package/src/eslintWebtypesGuardRule.test.ts +26 -0
- package/src/getFiles.ts +72 -0
- package/src/getFilesAtDepth.test.ts +158 -0
- package/src/getFilesAtDepth.ts +32 -0
- package/src/getFolder.test.ts +90 -0
- package/src/getFolder.ts +74 -0
- package/src/index.ts +60 -0
- package/src/insertIntoArray.test.ts +35 -0
- package/src/insertIntoArray.ts +10 -0
- package/src/listDirectoryEntries.ts +56 -0
- package/src/logAndRethrowPipelineError.test.ts +53 -0
- package/src/logAndRethrowPipelineError.ts +26 -0
- package/src/logAndSwallowPipelineError.test.ts +60 -0
- package/src/logAndSwallowPipelineError.ts +26 -0
- package/src/logMessage.test.ts +207 -0
- package/src/logMessage.ts +143 -0
- package/src/makeDirectory.test.ts +71 -0
- package/src/makeDirectory.ts +7 -0
- package/src/naturalSort.ts +8 -0
- package/src/replaceFileExtension.ts +9 -0
- package/src/test-runners.ts +43 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { join } from "node:path"
|
|
2
|
+
import { vol } from "memfs"
|
|
3
|
+
import { EmptyError, firstValueFrom } from "rxjs"
|
|
4
|
+
import { describe, expect, test } from "vitest"
|
|
5
|
+
import { captureLogMessage } from "./captureLogMessage.js"
|
|
6
|
+
import {
|
|
7
|
+
createRenameFileOrFolderObservable,
|
|
8
|
+
getLastItemInFilePath,
|
|
9
|
+
renameFileOrFolder,
|
|
10
|
+
} from "./createRenameFileOrFolder.js"
|
|
11
|
+
|
|
12
|
+
describe(getLastItemInFilePath.name, () => {
|
|
13
|
+
test("gets filename when no path", async () => {
|
|
14
|
+
expect(
|
|
15
|
+
getLastItemInFilePath("Star Wars (1977).mkv"),
|
|
16
|
+
).toBe("Star Wars (1977)")
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test("gets filename from shallow path", async () => {
|
|
20
|
+
expect(
|
|
21
|
+
getLastItemInFilePath(
|
|
22
|
+
"~/movies/Star Wars (1977) {edition-4K77}.mkv",
|
|
23
|
+
),
|
|
24
|
+
).toBe("Star Wars (1977) {edition-4K77}")
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test("gets filename from deep path", async () => {
|
|
28
|
+
expect(
|
|
29
|
+
getLastItemInFilePath(
|
|
30
|
+
"~/movies/Star Wars (1977)/Star Wars (1977) {edition-4K77}.mkv",
|
|
31
|
+
),
|
|
32
|
+
).toBe("Star Wars (1977) {edition-4K77}")
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test("gets folder name from path if no file", async () => {
|
|
36
|
+
expect(
|
|
37
|
+
getLastItemInFilePath("~/movies/Star Wars (1977)"),
|
|
38
|
+
).toBe("Star Wars (1977)")
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
describe(renameFileOrFolder.name, () => {
|
|
43
|
+
test("errors when the current file is missing", async () => {
|
|
44
|
+
expect(
|
|
45
|
+
firstValueFrom(
|
|
46
|
+
renameFileOrFolder({
|
|
47
|
+
newPath:
|
|
48
|
+
"/movies/Star Wars (1977)/Star Wars (1977) {edition-4K77}.mkv",
|
|
49
|
+
oldPath:
|
|
50
|
+
"/movies/Star Wars (1977)/Star Wars (1977).mkv",
|
|
51
|
+
}),
|
|
52
|
+
),
|
|
53
|
+
).rejects.toThrow("no such file or directory")
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test("errors when the renamed file already exists", async () => {
|
|
57
|
+
vol.fromJSON({
|
|
58
|
+
"/movies/Star Wars (1977)/Star Wars (1977).mkv": "",
|
|
59
|
+
"/movies/Star Wars (1977)/Star Wars (1977) {edition-4K77}.mkv":
|
|
60
|
+
"",
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
expect(
|
|
64
|
+
firstValueFrom(
|
|
65
|
+
renameFileOrFolder({
|
|
66
|
+
newPath:
|
|
67
|
+
"/movies/Star Wars (1977)/Star Wars (1977) {edition-4K77}.mkv",
|
|
68
|
+
oldPath:
|
|
69
|
+
"/movies/Star Wars (1977)/Star Wars (1977).mkv",
|
|
70
|
+
}),
|
|
71
|
+
),
|
|
72
|
+
).rejects.toThrow("already exists")
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test("renames a file", async () => {
|
|
76
|
+
vol.fromJSON({
|
|
77
|
+
"/movies/Star Wars (1977)/Star Wars (1977).mkv": "",
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
expect(
|
|
81
|
+
firstValueFrom(
|
|
82
|
+
renameFileOrFolder({
|
|
83
|
+
newPath:
|
|
84
|
+
"/movies/Star Wars (1977)/Star Wars (1977) {edition-4K77}.mkv",
|
|
85
|
+
oldPath:
|
|
86
|
+
"/movies/Star Wars (1977)/Star Wars (1977).mkv",
|
|
87
|
+
}),
|
|
88
|
+
),
|
|
89
|
+
).resolves.toBeUndefined()
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe(createRenameFileOrFolderObservable.name, () => {
|
|
94
|
+
test("completes when the old and new names are the same", async () => {
|
|
95
|
+
expect(
|
|
96
|
+
firstValueFrom(
|
|
97
|
+
createRenameFileOrFolderObservable({
|
|
98
|
+
fullPath:
|
|
99
|
+
"/movies/Star Wars (1977)/Star Wars (1977).mkv",
|
|
100
|
+
sourcePath: "/movies/Star Wars (1977)",
|
|
101
|
+
})("Star Wars (1977)"),
|
|
102
|
+
),
|
|
103
|
+
).rejects.toThrow(EmptyError)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test("errors when the current file is missing", async () => {
|
|
107
|
+
await captureLogMessage(
|
|
108
|
+
"error",
|
|
109
|
+
async (logMessageSpy) => {
|
|
110
|
+
await expect(
|
|
111
|
+
firstValueFrom(
|
|
112
|
+
createRenameFileOrFolderObservable({
|
|
113
|
+
fullPath:
|
|
114
|
+
"/movies/Star Wars (1977)/Star Wars (1977).mkv",
|
|
115
|
+
sourcePath: "/movies/Star Wars (1977)",
|
|
116
|
+
})("Star Wars (1977) {edition-4K77}"),
|
|
117
|
+
),
|
|
118
|
+
).rejects.toThrow(EmptyError)
|
|
119
|
+
|
|
120
|
+
expect(
|
|
121
|
+
logMessageSpy.mock.calls[0].find(
|
|
122
|
+
(error) =>
|
|
123
|
+
error instanceof Error && error.message,
|
|
124
|
+
).message,
|
|
125
|
+
).toContain("no such file or directory")
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test("errors when the renamed file already exists", async () => {
|
|
131
|
+
vol.fromJSON({
|
|
132
|
+
"/movies/Star Wars (1977)/Star Wars (1977).mkv": "",
|
|
133
|
+
"/movies/Star Wars (1977)/Star Wars (1977) {edition-4K77}.mkv":
|
|
134
|
+
"",
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
await captureLogMessage(
|
|
138
|
+
"error",
|
|
139
|
+
async (logMessageSpy) => {
|
|
140
|
+
await expect(
|
|
141
|
+
firstValueFrom(
|
|
142
|
+
createRenameFileOrFolderObservable({
|
|
143
|
+
fullPath:
|
|
144
|
+
"/movies/Star Wars (1977)/Star Wars (1977).mkv",
|
|
145
|
+
sourcePath: "/movies/Star Wars (1977)",
|
|
146
|
+
})("Star Wars (1977) {edition-4K77}"),
|
|
147
|
+
),
|
|
148
|
+
).rejects.toThrow(EmptyError)
|
|
149
|
+
|
|
150
|
+
expect(
|
|
151
|
+
logMessageSpy.mock.calls[0].find((text) =>
|
|
152
|
+
text.includes("already exists"),
|
|
153
|
+
),
|
|
154
|
+
).toContain("already exists")
|
|
155
|
+
},
|
|
156
|
+
)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test("renames a file", async () => {
|
|
160
|
+
vol.fromJSON({
|
|
161
|
+
"/movies/Star Wars (1977)/Star Wars (1977).mkv": "",
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
await captureLogMessage(
|
|
165
|
+
"info",
|
|
166
|
+
async (logMessageSpy) => {
|
|
167
|
+
await expect(
|
|
168
|
+
firstValueFrom(
|
|
169
|
+
createRenameFileOrFolderObservable({
|
|
170
|
+
fullPath:
|
|
171
|
+
"/movies/Star Wars (1977)/Star Wars (1977).mkv",
|
|
172
|
+
sourcePath: "/movies/Star Wars (1977)",
|
|
173
|
+
})("Star Wars (1977) {edition-4K77}"),
|
|
174
|
+
),
|
|
175
|
+
).resolves.toEqual({
|
|
176
|
+
newPath: join(
|
|
177
|
+
"/movies/Star Wars (1977)",
|
|
178
|
+
"Star Wars (1977) {edition-4K77}.mkv",
|
|
179
|
+
),
|
|
180
|
+
oldPath:
|
|
181
|
+
"/movies/Star Wars (1977)/Star Wars (1977).mkv",
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
const logMessageArgs = logMessageSpy.mock.calls[0]
|
|
185
|
+
|
|
186
|
+
expect(
|
|
187
|
+
logMessageArgs.find((text) =>
|
|
188
|
+
text.includes("RENAMED"),
|
|
189
|
+
),
|
|
190
|
+
).toContain("RENAMED")
|
|
191
|
+
|
|
192
|
+
expect(
|
|
193
|
+
logMessageArgs.find((text) =>
|
|
194
|
+
text.includes(
|
|
195
|
+
"/movies/Star Wars (1977)/Star Wars (1977).mkv",
|
|
196
|
+
),
|
|
197
|
+
),
|
|
198
|
+
).toContain(
|
|
199
|
+
"/movies/Star Wars (1977)/Star Wars (1977).mkv",
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
expect(
|
|
203
|
+
logMessageArgs.find((text) =>
|
|
204
|
+
text.includes(
|
|
205
|
+
join(
|
|
206
|
+
"/movies/Star Wars (1977)",
|
|
207
|
+
"Star Wars (1977) {edition-4K77}.mkv",
|
|
208
|
+
),
|
|
209
|
+
),
|
|
210
|
+
),
|
|
211
|
+
).toContain(
|
|
212
|
+
join(
|
|
213
|
+
"/movies/Star Wars (1977)",
|
|
214
|
+
"Star Wars (1977) {edition-4K77}.mkv",
|
|
215
|
+
),
|
|
216
|
+
)
|
|
217
|
+
},
|
|
218
|
+
)
|
|
219
|
+
})
|
|
220
|
+
})
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { rename, stat } from "node:fs/promises"
|
|
2
|
+
import { basename, extname, join } from "node:path"
|
|
3
|
+
import {
|
|
4
|
+
catchError,
|
|
5
|
+
concatMap,
|
|
6
|
+
filter,
|
|
7
|
+
from,
|
|
8
|
+
map,
|
|
9
|
+
of,
|
|
10
|
+
tap,
|
|
11
|
+
} from "rxjs"
|
|
12
|
+
|
|
13
|
+
import { logAndSwallowPipelineError } from "./logAndSwallowPipelineError.js"
|
|
14
|
+
import { logInfo } from "./logMessage.js"
|
|
15
|
+
|
|
16
|
+
export const getLastItemInFilePath = (filePath: string) =>
|
|
17
|
+
basename(filePath, extname(filePath))
|
|
18
|
+
|
|
19
|
+
export const renameFileOrFolder = ({
|
|
20
|
+
newPath,
|
|
21
|
+
oldPath,
|
|
22
|
+
}: {
|
|
23
|
+
newPath: string
|
|
24
|
+
oldPath: string
|
|
25
|
+
}) =>
|
|
26
|
+
from(stat(newPath)).pipe(
|
|
27
|
+
// If the file or folder doesn't already exist, this function throws an error. We can safely perform the rename in that case..
|
|
28
|
+
catchError(() => rename(oldPath, newPath)),
|
|
29
|
+
// If the `stat` succeeded, then we'll hit this `tap`. That means we can't rename the file because the one we're renaming to already exists.
|
|
30
|
+
tap((stats) => {
|
|
31
|
+
if (stats) {
|
|
32
|
+
throw "File or folder already exists for".concat(
|
|
33
|
+
" ",
|
|
34
|
+
'"',
|
|
35
|
+
getLastItemInFilePath(newPath),
|
|
36
|
+
'".',
|
|
37
|
+
" ",
|
|
38
|
+
`Cannot rename "${oldPath}".`,
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
}),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
export const createRenameFileOrFolderObservable =
|
|
45
|
+
({
|
|
46
|
+
fullPath: oldPath,
|
|
47
|
+
sourcePath,
|
|
48
|
+
}: {
|
|
49
|
+
fullPath: string
|
|
50
|
+
sourcePath: string
|
|
51
|
+
}) =>
|
|
52
|
+
(newName: string) =>
|
|
53
|
+
of({
|
|
54
|
+
oldPath,
|
|
55
|
+
newPath: join(sourcePath, newName).concat(
|
|
56
|
+
extname(oldPath),
|
|
57
|
+
),
|
|
58
|
+
}).pipe(
|
|
59
|
+
filter(({ newPath, oldPath }) => newPath !== oldPath),
|
|
60
|
+
concatMap(({ newPath, oldPath }) =>
|
|
61
|
+
renameFileOrFolder({
|
|
62
|
+
newPath,
|
|
63
|
+
oldPath,
|
|
64
|
+
}).pipe(
|
|
65
|
+
tap(() => {
|
|
66
|
+
logInfo("RENAMED", oldPath, newPath)
|
|
67
|
+
}),
|
|
68
|
+
// Emit { oldPath, newPath } instead of void so callers
|
|
69
|
+
// (renameDemos / renameMovieClipDownloads / nameAnimeEpisodes /
|
|
70
|
+
// nameTvShowEpisodes — every command that streams renameFile
|
|
71
|
+
// calls into job.results) get a useful per-rename record
|
|
72
|
+
// instead of a chain of nulls.
|
|
73
|
+
map(() => ({ newPath, oldPath })),
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
logAndSwallowPipelineError(
|
|
77
|
+
createRenameFileOrFolderObservable,
|
|
78
|
+
),
|
|
79
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { resolve } from "node:path"
|
|
2
|
+
import { fileURLToPath } from "node:url"
|
|
3
|
+
import { ESLint } from "eslint"
|
|
4
|
+
import { expect, test } from "vitest"
|
|
5
|
+
|
|
6
|
+
const workspaceRoot = resolve(
|
|
7
|
+
fileURLToPath(import.meta.url),
|
|
8
|
+
"../../../..",
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
const fixturePath = resolve(
|
|
12
|
+
workspaceRoot,
|
|
13
|
+
"packages/tools/src/__fixtures__/badBooleanName.ts",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
test("boolean variable without is/has prefix triggers naming-convention error", async () => {
|
|
17
|
+
const eslint = new ESLint({ cwd: workspaceRoot })
|
|
18
|
+
const [result] = await eslint.lintFiles([fixturePath])
|
|
19
|
+
// suppressedMessages items are LintMessage objects extended with `suppressions`
|
|
20
|
+
// (not wrapped in a { message } envelope), so spread them directly.
|
|
21
|
+
const namingViolations = [
|
|
22
|
+
...result.messages,
|
|
23
|
+
...result.suppressedMessages,
|
|
24
|
+
].filter(
|
|
25
|
+
(message) =>
|
|
26
|
+
message.ruleId ===
|
|
27
|
+
"@typescript-eslint/naming-convention",
|
|
28
|
+
)
|
|
29
|
+
expect(namingViolations.length).toBeGreaterThan(0)
|
|
30
|
+
}, 30_000)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { resolve } from "node:path"
|
|
2
|
+
import { fileURLToPath } from "node:url"
|
|
3
|
+
import { ESLint } from "eslint"
|
|
4
|
+
import { expect, test } from "vitest"
|
|
5
|
+
|
|
6
|
+
const workspaceRoot = resolve(
|
|
7
|
+
fileURLToPath(import.meta.url),
|
|
8
|
+
"../../../..",
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
const fixturePath = resolve(
|
|
12
|
+
workspaceRoot,
|
|
13
|
+
"packages/web/src/__eslintFixtures__/localApiShape.tsx",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
test("local API-shape type declarations in packages/web trigger no-restricted-syntax error", async () => {
|
|
17
|
+
const eslint = new ESLint({ cwd: workspaceRoot })
|
|
18
|
+
const [result] = await eslint.lintFiles([fixturePath])
|
|
19
|
+
const apiShapeViolations = [
|
|
20
|
+
...result.messages,
|
|
21
|
+
...result.suppressedMessages,
|
|
22
|
+
].filter(
|
|
23
|
+
(message) => message.ruleId === "no-restricted-syntax",
|
|
24
|
+
)
|
|
25
|
+
expect(apiShapeViolations.length).toBeGreaterThan(0)
|
|
26
|
+
}, 30_000)
|
package/src/getFiles.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import {
|
|
4
|
+
catchError,
|
|
5
|
+
concatAll,
|
|
6
|
+
concatMap,
|
|
7
|
+
defer,
|
|
8
|
+
EMPTY,
|
|
9
|
+
filter,
|
|
10
|
+
from,
|
|
11
|
+
map,
|
|
12
|
+
type Observable,
|
|
13
|
+
type OperatorFunction,
|
|
14
|
+
} from "rxjs"
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
createRenameFileOrFolderObservable,
|
|
18
|
+
getLastItemInFilePath,
|
|
19
|
+
} from "./createRenameFileOrFolder.js"
|
|
20
|
+
import { logAndRethrowPipelineError } from "./logAndRethrowPipelineError.js"
|
|
21
|
+
|
|
22
|
+
export type FileInfo = {
|
|
23
|
+
filename: string
|
|
24
|
+
fullPath: string
|
|
25
|
+
renameFile: (renamedFilename: string) => Observable<{
|
|
26
|
+
newPath: string
|
|
27
|
+
oldPath: string
|
|
28
|
+
}>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const filterFileAtPath = <PipelineValue>(
|
|
32
|
+
getFullPath: (pipelineValue: PipelineValue) => string,
|
|
33
|
+
): OperatorFunction<PipelineValue, PipelineValue> =>
|
|
34
|
+
concatMap((pipelineValue) =>
|
|
35
|
+
from(stat(getFullPath(pipelineValue))).pipe(
|
|
36
|
+
filter((stats) => stats.isFile()),
|
|
37
|
+
map(() => pipelineValue),
|
|
38
|
+
catchError((error) => {
|
|
39
|
+
if (error.code === "ENOENT") {
|
|
40
|
+
return EMPTY
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
throw error
|
|
44
|
+
}),
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
export const getFiles = ({
|
|
49
|
+
sourcePath,
|
|
50
|
+
}: {
|
|
51
|
+
sourcePath: string
|
|
52
|
+
}): Observable<FileInfo> =>
|
|
53
|
+
defer(() => readdir(sourcePath)).pipe(
|
|
54
|
+
concatAll(),
|
|
55
|
+
map((filePath) => ({
|
|
56
|
+
filename: getLastItemInFilePath(filePath),
|
|
57
|
+
fullPath: join(sourcePath, filePath),
|
|
58
|
+
})),
|
|
59
|
+
filterFileAtPath(({ fullPath }) => fullPath),
|
|
60
|
+
map(
|
|
61
|
+
({ filename, fullPath }) =>
|
|
62
|
+
({
|
|
63
|
+
filename,
|
|
64
|
+
fullPath,
|
|
65
|
+
renameFile: createRenameFileOrFolderObservable({
|
|
66
|
+
fullPath,
|
|
67
|
+
sourcePath,
|
|
68
|
+
}),
|
|
69
|
+
}) satisfies FileInfo as FileInfo,
|
|
70
|
+
),
|
|
71
|
+
logAndRethrowPipelineError(getFiles),
|
|
72
|
+
)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { join } from "node:path"
|
|
2
|
+
import { vol } from "memfs"
|
|
3
|
+
import { firstValueFrom, toArray } from "rxjs"
|
|
4
|
+
import { beforeEach, describe, expect, test } from "vitest"
|
|
5
|
+
|
|
6
|
+
import { captureLogMessage } from "./captureLogMessage.js"
|
|
7
|
+
import type { FileInfo } from "./getFiles.js"
|
|
8
|
+
import { getFilesAtDepth } from "./getFilesAtDepth.js"
|
|
9
|
+
|
|
10
|
+
describe(getFilesAtDepth.name, () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vol.fromJSON({
|
|
13
|
+
"/demos/Dolby/[Dolby] 747 (Audio) {FHD SDR & Dolby Atmos TrueHD}.mkv":
|
|
14
|
+
"",
|
|
15
|
+
"/movies/Star Wars (1977)/Star Wars (1977).mkv": "",
|
|
16
|
+
"/movies/Star Wars (1977)/Star Wars (1977) {edition-4K77}.mkv":
|
|
17
|
+
"",
|
|
18
|
+
"/movies/Super Mario Bros (1993)/Super Mario Bros (1993).mkv":
|
|
19
|
+
"",
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test("errors if source path can't be found", async () => {
|
|
24
|
+
captureLogMessage("error", async () => {
|
|
25
|
+
expect(
|
|
26
|
+
firstValueFrom(
|
|
27
|
+
getFilesAtDepth({
|
|
28
|
+
depth: 0,
|
|
29
|
+
sourcePath: "non-existent-path",
|
|
30
|
+
}),
|
|
31
|
+
),
|
|
32
|
+
).rejects.toThrow("ENOENT")
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test("emits no files when source only contains folders", async () => {
|
|
37
|
+
await expect(
|
|
38
|
+
firstValueFrom(
|
|
39
|
+
getFilesAtDepth({
|
|
40
|
+
depth: 0,
|
|
41
|
+
sourcePath: "/movies",
|
|
42
|
+
}).pipe(toArray()),
|
|
43
|
+
),
|
|
44
|
+
).resolves.toEqual([] satisfies FileInfo[])
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test("emits files when source contains files", async () => {
|
|
48
|
+
const expected: FileInfo[] = [
|
|
49
|
+
{
|
|
50
|
+
filename: "Star Wars (1977)",
|
|
51
|
+
fullPath: join(
|
|
52
|
+
"/movies/Star Wars (1977)",
|
|
53
|
+
"Star Wars (1977).mkv",
|
|
54
|
+
),
|
|
55
|
+
renameFile: expect.any(Function),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
filename: "Star Wars (1977) {edition-4K77}",
|
|
59
|
+
fullPath: join(
|
|
60
|
+
"/movies/Star Wars (1977)",
|
|
61
|
+
"Star Wars (1977) {edition-4K77}.mkv",
|
|
62
|
+
),
|
|
63
|
+
renameFile: expect.any(Function),
|
|
64
|
+
},
|
|
65
|
+
]
|
|
66
|
+
const actual = await firstValueFrom(
|
|
67
|
+
getFilesAtDepth({
|
|
68
|
+
depth: 0,
|
|
69
|
+
sourcePath: "/movies/Star Wars (1977)",
|
|
70
|
+
}).pipe(toArray()),
|
|
71
|
+
)
|
|
72
|
+
expect(actual).toEqual(expect.arrayContaining(expected))
|
|
73
|
+
expect(actual).toHaveLength(expected.length)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test("emits files when source contains files 1-level deep", async () => {
|
|
77
|
+
const expected: FileInfo[] = [
|
|
78
|
+
{
|
|
79
|
+
filename: "Star Wars (1977)",
|
|
80
|
+
fullPath: join(
|
|
81
|
+
"/movies/Star Wars (1977)",
|
|
82
|
+
"Star Wars (1977).mkv",
|
|
83
|
+
),
|
|
84
|
+
renameFile: expect.any(Function),
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
filename: "Star Wars (1977) {edition-4K77}",
|
|
88
|
+
fullPath: join(
|
|
89
|
+
"/movies/Star Wars (1977)",
|
|
90
|
+
"Star Wars (1977) {edition-4K77}.mkv",
|
|
91
|
+
),
|
|
92
|
+
renameFile: expect.any(Function),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
filename: "Super Mario Bros (1993)",
|
|
96
|
+
fullPath: join(
|
|
97
|
+
"/movies/Super Mario Bros (1993)",
|
|
98
|
+
"Super Mario Bros (1993).mkv",
|
|
99
|
+
),
|
|
100
|
+
renameFile: expect.any(Function),
|
|
101
|
+
},
|
|
102
|
+
]
|
|
103
|
+
const actual = await firstValueFrom(
|
|
104
|
+
getFilesAtDepth({
|
|
105
|
+
depth: 1,
|
|
106
|
+
sourcePath: "/movies",
|
|
107
|
+
}).pipe(toArray()),
|
|
108
|
+
)
|
|
109
|
+
expect(actual).toEqual(expect.arrayContaining(expected))
|
|
110
|
+
expect(actual).toHaveLength(expected.length)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test("emits files when source contains files 2-levels deep", async () => {
|
|
114
|
+
const expected: FileInfo[] = [
|
|
115
|
+
{
|
|
116
|
+
filename:
|
|
117
|
+
"[Dolby] 747 (Audio) {FHD SDR & Dolby Atmos TrueHD}",
|
|
118
|
+
fullPath: join(
|
|
119
|
+
"/demos/Dolby",
|
|
120
|
+
"[Dolby] 747 (Audio) {FHD SDR & Dolby Atmos TrueHD}.mkv",
|
|
121
|
+
),
|
|
122
|
+
renameFile: expect.any(Function),
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
filename: "Star Wars (1977)",
|
|
126
|
+
fullPath: join(
|
|
127
|
+
"/movies/Star Wars (1977)",
|
|
128
|
+
"Star Wars (1977).mkv",
|
|
129
|
+
),
|
|
130
|
+
renameFile: expect.any(Function),
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
filename: "Star Wars (1977) {edition-4K77}",
|
|
134
|
+
fullPath: join(
|
|
135
|
+
"/movies/Star Wars (1977)",
|
|
136
|
+
"Star Wars (1977) {edition-4K77}.mkv",
|
|
137
|
+
),
|
|
138
|
+
renameFile: expect.any(Function),
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
filename: "Super Mario Bros (1993)",
|
|
142
|
+
fullPath: join(
|
|
143
|
+
"/movies/Super Mario Bros (1993)",
|
|
144
|
+
"Super Mario Bros (1993).mkv",
|
|
145
|
+
),
|
|
146
|
+
renameFile: expect.any(Function),
|
|
147
|
+
},
|
|
148
|
+
]
|
|
149
|
+
const actual = await firstValueFrom(
|
|
150
|
+
getFilesAtDepth({
|
|
151
|
+
depth: 2,
|
|
152
|
+
sourcePath: "/",
|
|
153
|
+
}).pipe(toArray()),
|
|
154
|
+
)
|
|
155
|
+
expect(actual).toEqual(expect.arrayContaining(expected))
|
|
156
|
+
expect(actual).toHaveLength(expected.length)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { concat, concatMap, EMPTY, filter, iif } from "rxjs"
|
|
2
|
+
import { getFiles } from "./getFiles.js"
|
|
3
|
+
import { getFolder } from "./getFolder.js"
|
|
4
|
+
import { logAndRethrowPipelineError } from "./logAndRethrowPipelineError.js"
|
|
5
|
+
|
|
6
|
+
export const getFilesAtDepth = ({
|
|
7
|
+
depth,
|
|
8
|
+
sourcePath,
|
|
9
|
+
}: {
|
|
10
|
+
depth: number
|
|
11
|
+
sourcePath: string
|
|
12
|
+
}): ReturnType<typeof getFiles> =>
|
|
13
|
+
concat(
|
|
14
|
+
getFiles({
|
|
15
|
+
sourcePath,
|
|
16
|
+
}),
|
|
17
|
+
iif(
|
|
18
|
+
() => depth > 0,
|
|
19
|
+
getFolder({
|
|
20
|
+
sourcePath,
|
|
21
|
+
}).pipe(
|
|
22
|
+
concatMap((folderInfo) =>
|
|
23
|
+
getFilesAtDepth({
|
|
24
|
+
depth: depth - 1,
|
|
25
|
+
sourcePath: folderInfo.fullPath,
|
|
26
|
+
}),
|
|
27
|
+
),
|
|
28
|
+
filter(Boolean),
|
|
29
|
+
),
|
|
30
|
+
EMPTY,
|
|
31
|
+
),
|
|
32
|
+
).pipe(logAndRethrowPipelineError(getFilesAtDepth))
|