yt-grabber 1.0.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/.eslintrc.json +29 -0
- package/.prettierrc +19 -0
- package/.vscode/extensions.json +7 -0
- package/.vscode/settings.json +23 -0
- package/.yarnrc.yml +13 -0
- package/LICENSE +21 -0
- package/README.md +11 -0
- package/package.json +115 -0
- package/public/index.html +20 -0
- package/public/screenshots/cutting.png +0 -0
- package/public/screenshots/downloading.png +0 -0
- package/public/screenshots/editing.png +0 -0
- package/public/screenshots/errors.png +0 -0
- package/public/screenshots/settings.png +0 -0
- package/public/screenshots/tracklist.png +0 -0
- package/src/@types/global.d.ts +7 -0
- package/src/@types/i18next-scanner-webpack.d.ts +1 -0
- package/src/@types/stylus.d.ts +4 -0
- package/src/@types/svg.d.ts +1 -0
- package/src/App.styl +24 -0
- package/src/App.tsx +31 -0
- package/src/bootstrap.tsx +30 -0
- package/src/common/CancellablePromise.ts +22 -0
- package/src/common/ComponentDisplayMode.ts +8 -0
- package/src/common/Delay.ts +3 -0
- package/src/common/FileSystem.ts +171 -0
- package/src/common/Helpers.ts +270 -0
- package/src/common/Mappings.ts +14 -0
- package/src/common/PuppeteerOptions.ts +45 -0
- package/src/common/Selectors.ts +21 -0
- package/src/common/Store.ts +108 -0
- package/src/common/Theme.ts +4 -0
- package/src/common/Youtube.ts +80 -0
- package/src/components/appBar/AppBar.styl +22 -0
- package/src/components/appBar/AppBar.tsx +73 -0
- package/src/components/directoryPicker/DirectoryPicker.tsx +44 -0
- package/src/components/fileField/FileField.styl +3 -0
- package/src/components/fileField/FileField.tsx +152 -0
- package/src/components/languagePicker/LanguagePicker.styl +38 -0
- package/src/components/languagePicker/LanguagePicker.tsx +145 -0
- package/src/components/logo/Logo.tsx +15 -0
- package/src/components/modals/DetailsModal.styl +9 -0
- package/src/components/modals/DetailsModal.tsx +85 -0
- package/src/components/numberField/NumberField.styl +13 -0
- package/src/components/numberField/NumberField.tsx +154 -0
- package/src/components/progress/Progress.styl +15 -0
- package/src/components/progress/Progress.tsx +18 -0
- package/src/components/splitButton/SplitButton.styl +0 -0
- package/src/components/splitButton/SplitButton.tsx +125 -0
- package/src/components/themePicker/ThemePicker.styl +19 -0
- package/src/components/themePicker/ThemePicker.tsx +65 -0
- package/src/components/themeSwitcher/ThemeSwitcher.styl +10 -0
- package/src/components/themeSwitcher/ThemeSwitcher.tsx +43 -0
- package/src/components/youtube/formatSelector/FormatSelector.styl +3 -0
- package/src/components/youtube/formatSelector/FormatSelector.tsx +202 -0
- package/src/components/youtube/inputPanel/InputPanel.styl +7 -0
- package/src/components/youtube/inputPanel/InputPanel.tsx +189 -0
- package/src/components/youtube/mediaInfoPanel/MediaInfoPanel.styl +80 -0
- package/src/components/youtube/mediaInfoPanel/MediaInfoPanel.tsx +113 -0
- package/src/components/youtube/trackList/TrackList.styl +64 -0
- package/src/components/youtube/trackList/TrackList.tsx +258 -0
- package/src/enums/DataResponse.ts +5 -0
- package/src/enums/Media.ts +16 -0
- package/src/enums/MediaFormat.ts +10 -0
- package/src/enums/MimeTypes.ts +14 -0
- package/src/hooks/useCancellablePromises.ts +25 -0
- package/src/hooks/useClickCounter.ts +24 -0
- package/src/hooks/useData.ts +61 -0
- package/src/hooks/useMultiClickHandler.ts +41 -0
- package/src/i18next.ts +33 -0
- package/src/index.ts +65 -0
- package/src/react/actions/Action.ts +3 -0
- package/src/react/actions/AppActions.ts +41 -0
- package/src/react/contexts/AppContext.tsx +51 -0
- package/src/react/contexts/AppThemeContext.tsx +38 -0
- package/src/react/contexts/DataContext copy.tsx +76 -0
- package/src/react/contexts/DataContext.tsx +41 -0
- package/src/react/hooks/useAppTheme.ts +14 -0
- package/src/react/reducers/AppReducer.tsx +45 -0
- package/src/react/reducers/Reducer.ts +7 -0
- package/src/react/states/AppState.ts +29 -0
- package/src/react/states/State.ts +29 -0
- package/src/renderer.tsx +13 -0
- package/src/resources/bin/yt-dlp.exe +0 -0
- package/src/resources/fonts/Baloo-Regular.ttf +0 -0
- package/src/resources/fonts/Lato-Black.ttf +0 -0
- package/src/resources/fonts/Lato-BlackItalic.ttf +0 -0
- package/src/resources/fonts/Lato-Bold.ttf +0 -0
- package/src/resources/fonts/Lato-BoldItalic.ttf +0 -0
- package/src/resources/fonts/Lato-Italic.ttf +0 -0
- package/src/resources/fonts/Lato-Light.ttf +0 -0
- package/src/resources/fonts/Lato-LightItalic.ttf +0 -0
- package/src/resources/fonts/Lato-Regular.ttf +0 -0
- package/src/resources/fonts/Lato-Thin.ttf +0 -0
- package/src/resources/fonts/Lato-ThinItalic.ttf +0 -0
- package/src/resources/fonts/Material-Icons.woff2 +0 -0
- package/src/resources/icons/favicon-16x16.png +0 -0
- package/src/resources/icons/favicon-32x32.png +0 -0
- package/src/resources/icons/favicon.ico +0 -0
- package/src/resources/icons/logo-shape.png +0 -0
- package/src/resources/icons/logo-shape.svg +59 -0
- package/src/resources/images/loading.svg +28 -0
- package/src/resources/images/logo.png +0 -0
- package/src/resources/locales/de-DE/flag.svg +1 -0
- package/src/resources/locales/de-DE/translation.json +44 -0
- package/src/resources/locales/en-GB/flag.svg +43 -0
- package/src/resources/locales/en-GB/translation.json +44 -0
- package/src/resources/locales/pl-PL/flag.svg +36 -0
- package/src/resources/locales/pl-PL/translation.json +44 -0
- package/src/styles/MaterialThemes.ts +331 -0
- package/src/styles/fonts.styl +71 -0
- package/src/styles/mixins.styl +22 -0
- package/src/tests/CompleteTracksMock.ts +17384 -0
- package/src/tests/MissingDetailsTracksMock.ts +7737 -0
- package/src/theme/ColorThemes.ts +190 -0
- package/src/theme/Colors.ts +92 -0
- package/src/theme/Shadows.ts +9 -0
- package/src/theme/Shape.ts +7 -0
- package/src/theme/Theme.ts +24 -0
- package/src/theme/Typography.ts +56 -0
- package/src/views/development/DevelopmentView.styl +22 -0
- package/src/views/development/DevelopmentView.tsx +57 -0
- package/src/views/home/HomeView.styl +60 -0
- package/src/views/home/HomeView.tsx +505 -0
- package/src/views/settings/SettingsView.styl +27 -0
- package/src/views/settings/SettingsView.tsx +255 -0
- package/tsconfig.json +20 -0
- package/webpack.config.ts +226 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import $_ from "lodash";
|
|
2
|
+
import {setTimeout} from "node:timers/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import {Browser, ElementHandle, Frame, Page} from "puppeteer";
|
|
5
|
+
import puppeteer from "puppeteer-extra";
|
|
6
|
+
import pluginStealth from "puppeteer-extra-plugin-stealth";
|
|
7
|
+
|
|
8
|
+
export const setTitle = async (title: string, page: Page) => {
|
|
9
|
+
return page.evaluate((data) => (document.title = data), title);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const isHeadless = async (browser: Browser) => {
|
|
13
|
+
return $_.includes(await browser.version(), "Headless");
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const decode = (value: string) => {
|
|
17
|
+
return Buffer.from(value, "base64").toString("ascii");
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const encode = (value: string) => {
|
|
21
|
+
return Buffer.from(value).toString("base64");
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const getOutputDir = (config: any) => {
|
|
25
|
+
return path.normalize(path.resolve(config.outputDir));
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const doubleClick = async (selector: string, page: Page) => {
|
|
29
|
+
await page.click(selector);
|
|
30
|
+
await page.click(selector, { count: 2 });
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const dbClickSlow = async (selector: string, page: Page) => {
|
|
34
|
+
const element = await page.$(selector);
|
|
35
|
+
|
|
36
|
+
if (!element) return;
|
|
37
|
+
|
|
38
|
+
await element.click();
|
|
39
|
+
await setTimeout(200);
|
|
40
|
+
await element.click({ count: 2 });
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const tripleClick = async (selector: string | ElementHandle<Element>, page: Page) => {
|
|
44
|
+
const input = typeof selector === "string" ? await page.$(selector) : selector;
|
|
45
|
+
|
|
46
|
+
if (!input) return;
|
|
47
|
+
|
|
48
|
+
await input.click({ count: 3 });
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const clickOutOfView = async (selector: string, page: Page) => {
|
|
52
|
+
const link = await page.$(selector);
|
|
53
|
+
if (link) return clickOnPage(selector, page);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const clickOnPage = async (selector: string, page: Page) => {
|
|
57
|
+
return page.evaluate((value) => document.querySelector<HTMLElement>(value)?.click(), selector);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const clickXPathOutOfView = async (xpath: string, page: Page) => {
|
|
61
|
+
const element = await page.waitForSelector(xpath);
|
|
62
|
+
if (element) {
|
|
63
|
+
await page.evaluate<any>((item) => item.click(), (await page.$$(xpath))[0]);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const clearInput = async (selector: string | ElementHandle<Element>, page: Page) => {
|
|
68
|
+
await tripleClick(selector, page);
|
|
69
|
+
await page.keyboard.press("Delete");
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const getElementTextContent = async (xPath: string, page: Page) => {
|
|
73
|
+
const element = await page.$$(xPath);
|
|
74
|
+
|
|
75
|
+
return page.evaluate((el: any) => el.textContent, $_.first(element));
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const clickInShadowRoot = async (xpath: string, page: Page) => {
|
|
79
|
+
try {
|
|
80
|
+
const element = await page.waitForSelector(xpath, { timeout: 8000 });
|
|
81
|
+
|
|
82
|
+
if (element) {
|
|
83
|
+
return page.evaluate((item) => {
|
|
84
|
+
return item.shadowRoot
|
|
85
|
+
?.querySelector<HTMLButtonElement>("button[slot='action-slot'].second-button")
|
|
86
|
+
?.click();
|
|
87
|
+
}, (await page.$$(xpath))[0]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return Promise.resolve();
|
|
91
|
+
} catch (error) {
|
|
92
|
+
return Promise.resolve(error);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const getElementFromShadowRoot = async (xpath: string, page: Page) => {
|
|
97
|
+
try {
|
|
98
|
+
const element = await page.waitForSelector(xpath, { timeout: 8000 });
|
|
99
|
+
|
|
100
|
+
if (element) {
|
|
101
|
+
return page.evaluate((item) => {
|
|
102
|
+
return item.shadowRoot
|
|
103
|
+
?.querySelector<HTMLButtonElement>("button[slot='action-slot'].second-button")
|
|
104
|
+
?.click();
|
|
105
|
+
}, (await page.$$(xpath))[0]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return error;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const setRequestInterceptors = async (page: Page, filters: any[]) => {
|
|
115
|
+
if ($_.isEmpty(filters)) return;
|
|
116
|
+
|
|
117
|
+
await page.setRequestInterception(true);
|
|
118
|
+
|
|
119
|
+
page.on("request", (req) => {
|
|
120
|
+
if ($_.includes(filters, req.resourceType())) {
|
|
121
|
+
return req.abort();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return req.continue();
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const useStealth = async () => {
|
|
129
|
+
const shouldUseStealth = $_.get(global, "config.useStealth");
|
|
130
|
+
|
|
131
|
+
if (!shouldUseStealth) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
puppeteer.use(pluginStealth());
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const runScript = async (content: string, page: Page) => {
|
|
139
|
+
return page.evaluate((data) => Promise.resolve(eval(data)), content);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const waitUntilSelectorTextContains = async (selector: string, text: string, page: Page) =>
|
|
143
|
+
page.waitForFunction(
|
|
144
|
+
(selectorString: any, textString: any) => {
|
|
145
|
+
const item = document.querySelector(selectorString);
|
|
146
|
+
|
|
147
|
+
return item && item.textContent && item.textContent.indexOf(textString) !== -1;
|
|
148
|
+
},
|
|
149
|
+
{ polling: "mutation" },
|
|
150
|
+
selector,
|
|
151
|
+
text,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
export const waitUntilSelectorTextEquals = async (selector: string, text: string, page: Page) =>
|
|
155
|
+
page.waitForFunction(
|
|
156
|
+
(selectorString: any, textString: any) => {
|
|
157
|
+
const item = document.querySelector(selectorString);
|
|
158
|
+
|
|
159
|
+
return item && item.textContent === textString;
|
|
160
|
+
},
|
|
161
|
+
{ polling: "mutation" },
|
|
162
|
+
selector,
|
|
163
|
+
text,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
export const waitForBufferResponse = async (
|
|
167
|
+
url: string,
|
|
168
|
+
reqMethod: string,
|
|
169
|
+
status: number,
|
|
170
|
+
size: { min: number },
|
|
171
|
+
expectedHeaders: any,
|
|
172
|
+
page: Page,
|
|
173
|
+
) =>
|
|
174
|
+
new Promise((resolve: any, reject: any) => {
|
|
175
|
+
page.on("response", async (response: any) => {
|
|
176
|
+
if (!$_.includes(response.url(), url) || response.request().method() !== reqMethod) return response;
|
|
177
|
+
|
|
178
|
+
const buffer = await response.buffer();
|
|
179
|
+
const statusOk = response.status() === status;
|
|
180
|
+
const sizeOk = buffer.length >= size.min;
|
|
181
|
+
|
|
182
|
+
if (!statusOk || !sizeOk) return reject();
|
|
183
|
+
|
|
184
|
+
$_.forEach(expectedHeaders, (value: string, key: string) => {
|
|
185
|
+
if (!$_.includes($_.lowerCase(response.headers()[key]), $_.lowerCase(value))) return reject();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return resolve(buffer);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
export const waitForFileContentResponse = async (
|
|
193
|
+
url: string,
|
|
194
|
+
reqMethod: string,
|
|
195
|
+
status: number,
|
|
196
|
+
expectedHeaders: any,
|
|
197
|
+
page: Page,
|
|
198
|
+
): Promise<any> =>
|
|
199
|
+
new Promise((resolve: any, reject: any) => {
|
|
200
|
+
page.on("response", async (response: any) => {
|
|
201
|
+
if (!$_.includes(response.url(), url) || response.request().method() !== reqMethod) {
|
|
202
|
+
return response;
|
|
203
|
+
}
|
|
204
|
+
const statusOk = response.status() === status;
|
|
205
|
+
|
|
206
|
+
if (!statusOk) return reject();
|
|
207
|
+
|
|
208
|
+
$_.forEach(expectedHeaders, (value: string, key: string) => {
|
|
209
|
+
if (!$_.includes($_.lowerCase(response.headers()[key]), $_.lowerCase(value))) {
|
|
210
|
+
return reject();
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
// const parsed = contentDisposition.parse(response.headers()["content-disposition"]);
|
|
214
|
+
return resolve(response.buffer());
|
|
215
|
+
// return resolve($_.get(parsed, "parameters.filename"));
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
export const count = async (selector: string, frame: Frame) => {
|
|
220
|
+
return (await frame.$$(selector)).length;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export const exists = async (selector: string, frame: Frame) => {
|
|
224
|
+
return !!(await frame.$(selector));
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
export const clickLink = async (selector: string, page: Page) =>
|
|
228
|
+
Promise.all([page.waitForNavigation(), page.click(selector)]);
|
|
229
|
+
|
|
230
|
+
export const getXPathElement = async (xpath: string, page: Page) => $_.first(await page.$$(xpath));
|
|
231
|
+
|
|
232
|
+
export const queryShadowDom = async (page: Page, selector: string) => {
|
|
233
|
+
return page.evaluateHandle((input) => {
|
|
234
|
+
const shadowSeparator = "::shadow";
|
|
235
|
+
const parts = input.split(shadowSeparator).map((item: string) => item.trim());
|
|
236
|
+
const endsWithShadow = input.endsWith(shadowSeparator);
|
|
237
|
+
|
|
238
|
+
const element = parts.reduce((total: any, current: any, index: number, arr: string[]) => {
|
|
239
|
+
const selected = total.querySelector(current);
|
|
240
|
+
if (index < arr.length - 1 || endsWithShadow) {
|
|
241
|
+
return selected.shadowRoot;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return selected;
|
|
245
|
+
}, document);
|
|
246
|
+
|
|
247
|
+
return element;
|
|
248
|
+
}, selector);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
export const retry = async (handler: any, params: any[], attempts = 5): Promise<any> => {
|
|
252
|
+
try {
|
|
253
|
+
return await handler(...params);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (attempts <= 1) {
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
return retry(handler, params, attempts - 1);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export const formatFileSize = (sizeInBytes: number, decimals = 2) => {
|
|
263
|
+
if (sizeInBytes === 0) return "0 Bytes";
|
|
264
|
+
|
|
265
|
+
const k = 1024;
|
|
266
|
+
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB"];
|
|
267
|
+
const i = Math.floor(Math.log(sizeInBytes) / Math.log(k));
|
|
268
|
+
|
|
269
|
+
return parseFloat((sizeInBytes / Math.pow(k, i)).toFixed(decimals)) + " " + sizes[i];
|
|
270
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface IStringTMap<T> { [key: string]: T; }
|
|
2
|
+
export interface INumberTMap<T> { [key: number]: T; }
|
|
3
|
+
|
|
4
|
+
export interface IStringAnyMap extends IStringTMap<any> {}
|
|
5
|
+
export interface INumberAnyMap extends INumberTMap<any> {}
|
|
6
|
+
|
|
7
|
+
export interface IStringStringMap extends IStringTMap<string> {}
|
|
8
|
+
export interface INumberStringMap extends INumberTMap<string> {}
|
|
9
|
+
|
|
10
|
+
export interface IStringNumberMap extends IStringTMap<number> {}
|
|
11
|
+
export interface INumberNumberMap extends INumberTMap<number> {}
|
|
12
|
+
|
|
13
|
+
export interface IStringBooleanMap extends IStringTMap<boolean> {}
|
|
14
|
+
export interface INumberBooleanMap extends INumberTMap<boolean> {}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {LaunchOptions} from "puppeteer";
|
|
2
|
+
|
|
3
|
+
const width = 1280;
|
|
4
|
+
const height = 800;
|
|
5
|
+
|
|
6
|
+
const options: LaunchOptions = {
|
|
7
|
+
headless: false,
|
|
8
|
+
userDataDir: "./output/user-data-dir",
|
|
9
|
+
defaultViewport: {
|
|
10
|
+
width,
|
|
11
|
+
height,
|
|
12
|
+
},
|
|
13
|
+
timeout: 15000,
|
|
14
|
+
// executablePath: "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
|
15
|
+
devtools: false,
|
|
16
|
+
args: [
|
|
17
|
+
`--disable-infobars`,
|
|
18
|
+
`--window-size=${width},${height}`,
|
|
19
|
+
`--disable-extensions`,
|
|
20
|
+
`--mute-audio`,
|
|
21
|
+
`--disable-background-timer-throttling`,
|
|
22
|
+
`--no-sandbox`,
|
|
23
|
+
`--incognito`,
|
|
24
|
+
"--disable-setuid-sandbox",
|
|
25
|
+
"--disable-autofill-keyboard-accessory-view",
|
|
26
|
+
"--disable-password-generation",
|
|
27
|
+
"--disable-save-password-bubble",
|
|
28
|
+
`--disable-backgrounding-occluded-windows`,
|
|
29
|
+
`--disable-renderer-backgrounding`,
|
|
30
|
+
`--disable-dev-shm-usage`,
|
|
31
|
+
"--disable-prompt-on-repost",
|
|
32
|
+
"--disable-notifications",
|
|
33
|
+
`--disable-accelerated-2d-canvas`,
|
|
34
|
+
"--hide-crash-restore-bubble",
|
|
35
|
+
"--no-zygote",
|
|
36
|
+
"--webview-disable-safebrowsing-support",
|
|
37
|
+
"--autoplay-policy=no-user-gesture-required",
|
|
38
|
+
"--use-fake-ui-for-media-stream",
|
|
39
|
+
"--disable-crash-reporter",
|
|
40
|
+
"--disable-site-isolation-trials",
|
|
41
|
+
],
|
|
42
|
+
ignoreDefaultArgs: ["--enable-automation"],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default options;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import $_ from "lodash";
|
|
2
|
+
|
|
3
|
+
export const Css: any = {
|
|
4
|
+
ReactAppElement: "#react-app",
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const XPath: any = {};
|
|
8
|
+
|
|
9
|
+
export const getInput = (attribute: string, value: string) => `input[${attribute} = "${value}"]`;
|
|
10
|
+
|
|
11
|
+
export const getInputByName = (name: string) => `input[name="${name}"]`;
|
|
12
|
+
|
|
13
|
+
export const getInputByType = (type: string) => `input[type="${name}"]`;
|
|
14
|
+
|
|
15
|
+
export const getDivByTitle = (title: string) => `div[title="${title}"]`;
|
|
16
|
+
|
|
17
|
+
export const getElement = (tag: string, attributes: { [key: string]: string }) =>
|
|
18
|
+
`${tag}${$_.join(
|
|
19
|
+
$_.map(attributes, (v, k) => `[${k}="${v}"]`),
|
|
20
|
+
"",
|
|
21
|
+
)}`;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {Schema} from "electron-store";
|
|
2
|
+
|
|
3
|
+
import {Format} from "../components/youtube/formatSelector/FormatSelector";
|
|
4
|
+
|
|
5
|
+
export type ApplicationOptions = {
|
|
6
|
+
outputDirectory?: string;
|
|
7
|
+
albumOutputTemplate?: string;
|
|
8
|
+
playlistOutputTemplate?: string;
|
|
9
|
+
videoOutputTemplate?: string;
|
|
10
|
+
trackOutputTemplate?: string;
|
|
11
|
+
concurrency?: number;
|
|
12
|
+
quality?: number;
|
|
13
|
+
format?: Format;
|
|
14
|
+
debugMode?: boolean;
|
|
15
|
+
url?: string;
|
|
16
|
+
language?: string;
|
|
17
|
+
alwaysOverwrite?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export interface IStore {
|
|
21
|
+
options: {
|
|
22
|
+
headless: boolean;
|
|
23
|
+
};
|
|
24
|
+
application: ApplicationOptions;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const StoreSchema: Schema<IStore> = {
|
|
28
|
+
options: {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
headless: {
|
|
32
|
+
type: "boolean",
|
|
33
|
+
default: false,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
default: {},
|
|
37
|
+
},
|
|
38
|
+
application: {
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
url: {
|
|
42
|
+
type: "string",
|
|
43
|
+
default: "",
|
|
44
|
+
},
|
|
45
|
+
outputDirectory: {
|
|
46
|
+
type: "string",
|
|
47
|
+
default: "/output",
|
|
48
|
+
},
|
|
49
|
+
albumOutputTemplate: {
|
|
50
|
+
type: "string",
|
|
51
|
+
default: "{{artist}}/[{{releaseYear}}] {{albumTitle}}/{{trackNo}} - {{trackTitle}}",
|
|
52
|
+
},
|
|
53
|
+
playlistOutputTemplate: {
|
|
54
|
+
type: "string",
|
|
55
|
+
default: "{{albumTitle}}/{{trackTitle}}",
|
|
56
|
+
},
|
|
57
|
+
videoOutputTemplate: {
|
|
58
|
+
type: "string",
|
|
59
|
+
default: "{{artist}} - {{trackTitle}}",
|
|
60
|
+
},
|
|
61
|
+
trackOutputTemplate: {
|
|
62
|
+
type: "string",
|
|
63
|
+
default: "{{artist}} - {{trackTitle}}",
|
|
64
|
+
},
|
|
65
|
+
concurrency: {
|
|
66
|
+
type: "integer",
|
|
67
|
+
default: 1
|
|
68
|
+
},
|
|
69
|
+
quality: {
|
|
70
|
+
type: "integer",
|
|
71
|
+
default: 0
|
|
72
|
+
},
|
|
73
|
+
format: {
|
|
74
|
+
type: "object",
|
|
75
|
+
properties: {
|
|
76
|
+
type: {
|
|
77
|
+
type: "string",
|
|
78
|
+
enum: ["video", "audio"],
|
|
79
|
+
default: "audio",
|
|
80
|
+
},
|
|
81
|
+
extension: {
|
|
82
|
+
type: "string",
|
|
83
|
+
enum: ["mp3", "m4a", "flac", "wav", "opus", "mp4", "mkv"],
|
|
84
|
+
default: "mp3",
|
|
85
|
+
},
|
|
86
|
+
audioQuality: {
|
|
87
|
+
type: "integer",
|
|
88
|
+
default: 0,
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
debugMode: {
|
|
93
|
+
type: "boolean",
|
|
94
|
+
default: false
|
|
95
|
+
},
|
|
96
|
+
language: {
|
|
97
|
+
type: "string"
|
|
98
|
+
},
|
|
99
|
+
alwaysOverwrite: {
|
|
100
|
+
type: "boolean",
|
|
101
|
+
default: false
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
default: {},
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export default StoreSchema;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {MediaFormat} from "../enums/Media";
|
|
2
|
+
|
|
3
|
+
export type TrackInfo = {
|
|
4
|
+
album: string;
|
|
5
|
+
artist: string;
|
|
6
|
+
channel: string;
|
|
7
|
+
creators: string[];
|
|
8
|
+
duration: number;
|
|
9
|
+
id: string;
|
|
10
|
+
original_url: string;
|
|
11
|
+
playlist: string;
|
|
12
|
+
playlist_title: string;
|
|
13
|
+
playlist_autonumber: number;
|
|
14
|
+
playlist_count: number;
|
|
15
|
+
release_year: number;
|
|
16
|
+
title: string;
|
|
17
|
+
thumbnail: string;
|
|
18
|
+
formats: FormatInfo[];
|
|
19
|
+
timestamp: number;
|
|
20
|
+
filesize_approx: number;
|
|
21
|
+
thumbnails: Thumbnail[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type AlbumInfo = {
|
|
25
|
+
artist: string;
|
|
26
|
+
title: string;
|
|
27
|
+
releaseYear: number;
|
|
28
|
+
tracksNumber: number;
|
|
29
|
+
duration: number;
|
|
30
|
+
thumbnail: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type Thumbnail = {
|
|
34
|
+
width?: number;
|
|
35
|
+
height?: number;
|
|
36
|
+
url: string;
|
|
37
|
+
id: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type TrackStatusInfo = {
|
|
41
|
+
trackId: string;
|
|
42
|
+
percent: number;
|
|
43
|
+
totalSize: number;
|
|
44
|
+
completed?: boolean;
|
|
45
|
+
status?: string;
|
|
46
|
+
error?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type FormatInfo = {
|
|
50
|
+
ext: string;
|
|
51
|
+
acodec: string;
|
|
52
|
+
vcodec: string;
|
|
53
|
+
audio_ext: string;
|
|
54
|
+
video_ext: string;
|
|
55
|
+
format_id: string;
|
|
56
|
+
format_note: string;
|
|
57
|
+
filesize: number;
|
|
58
|
+
fps: number;
|
|
59
|
+
quality: number;
|
|
60
|
+
has_drm: boolean;
|
|
61
|
+
width: number;
|
|
62
|
+
height: number;
|
|
63
|
+
resolution: string;
|
|
64
|
+
protocol: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type Format = {
|
|
68
|
+
type: MediaFormat;
|
|
69
|
+
name: string;
|
|
70
|
+
extension: string;
|
|
71
|
+
protocol: string;
|
|
72
|
+
id: string;
|
|
73
|
+
width: number;
|
|
74
|
+
height: number;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type TrackCut = {
|
|
78
|
+
from: number;
|
|
79
|
+
to: number;
|
|
80
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
.app-bar
|
|
2
|
+
padding: 0
|
|
3
|
+
|
|
4
|
+
.wrapper
|
|
5
|
+
padding: 0 12px
|
|
6
|
+
|
|
7
|
+
.logo
|
|
8
|
+
margin-right: 1em
|
|
9
|
+
width: 2em
|
|
10
|
+
height: 2em
|
|
11
|
+
|
|
12
|
+
.title
|
|
13
|
+
font-family: "Baloo"
|
|
14
|
+
font-size: 1.8rem
|
|
15
|
+
line-height: 1.8
|
|
16
|
+
|
|
17
|
+
.icon
|
|
18
|
+
margin-left: 0
|
|
19
|
+
|
|
20
|
+
svg
|
|
21
|
+
width: 1.4em
|
|
22
|
+
height: 1.4em
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import CloseIcon from "@mui/icons-material/Close";
|
|
4
|
+
import SettingsIcon from "@mui/icons-material/Settings";
|
|
5
|
+
import {Stack} from "@mui/material";
|
|
6
|
+
import ApplicationBar from "@mui/material/AppBar";
|
|
7
|
+
import Box from "@mui/material/Box";
|
|
8
|
+
import Container from "@mui/material/Container";
|
|
9
|
+
import IconButton from "@mui/material/IconButton";
|
|
10
|
+
import Toolbar from "@mui/material/Toolbar";
|
|
11
|
+
import Tooltip from "@mui/material/Tooltip";
|
|
12
|
+
import Typography from "@mui/material/Typography";
|
|
13
|
+
|
|
14
|
+
import ComponentDisplayMode from "../../common/ComponentDisplayMode";
|
|
15
|
+
import {useClickCounter} from "../../hooks/useClickCounter";
|
|
16
|
+
import {useAppContext} from "../../react/contexts/AppContext";
|
|
17
|
+
import LanguagePicker from "../languagePicker/LanguagePicker";
|
|
18
|
+
import Logo from "../logo/Logo";
|
|
19
|
+
import Styles from "./AppBar.styl";
|
|
20
|
+
|
|
21
|
+
function AppBar() {
|
|
22
|
+
const { state, actions } = useAppContext();
|
|
23
|
+
const { onClick } = useClickCounter(() => handleOpenDevelopment(), 3, 500);
|
|
24
|
+
|
|
25
|
+
const handleOpenDevelopment = () => {
|
|
26
|
+
actions.setLocation("/development");
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const handleOpenSettings = () => {
|
|
30
|
+
actions.setLocation("/settings");
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const handleClose = () => {
|
|
34
|
+
actions.setLocation("/");
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const createSettingsButton = () => {
|
|
38
|
+
if (state.location !== "/") {
|
|
39
|
+
return (
|
|
40
|
+
<IconButton onClick={handleClose} color="inherit" className={Styles.icon}>
|
|
41
|
+
<CloseIcon />
|
|
42
|
+
</IconButton>
|
|
43
|
+
);
|
|
44
|
+
} else {
|
|
45
|
+
return (
|
|
46
|
+
<IconButton onClick={handleOpenSettings} color="inherit" className={Styles.icon}>
|
|
47
|
+
<SettingsIcon />
|
|
48
|
+
</IconButton>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<ApplicationBar elevation={0} position="static" className={Styles.appBar}>
|
|
55
|
+
<Container maxWidth="xl" className={Styles.wrapper}>
|
|
56
|
+
<Toolbar disableGutters variant="dense">
|
|
57
|
+
<Logo onClick={onClick} className={Styles.logo} />
|
|
58
|
+
<Typography className={Styles.title} variant="h6" noWrap>
|
|
59
|
+
YT GRABBER
|
|
60
|
+
</Typography>
|
|
61
|
+
<Box sx={{flexGrow: 1}}></Box>
|
|
62
|
+
<Stack direction="row" gap={1}>
|
|
63
|
+
<LanguagePicker showArrow={false} mode={ComponentDisplayMode.Minimal} />
|
|
64
|
+
<Tooltip title={state.location === "/settings" ? "Close settings" : "Open settings"}>
|
|
65
|
+
{createSettingsButton()}
|
|
66
|
+
</Tooltip>
|
|
67
|
+
</Stack>
|
|
68
|
+
</Toolbar>
|
|
69
|
+
</Container>
|
|
70
|
+
</ApplicationBar>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
export default AppBar;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React, {useState} from "react";
|
|
2
|
+
|
|
3
|
+
import {Button, TextField, TextFieldProps} from "@mui/material";
|
|
4
|
+
|
|
5
|
+
const DirectoryPicker = (props: TextFieldProps<"outlined">) => {
|
|
6
|
+
const {className, value, ...rest} = props;
|
|
7
|
+
const [directoryPath, setDirectoryPath] = useState<string>("");
|
|
8
|
+
|
|
9
|
+
const handleDirectorySelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
10
|
+
const files = event.target.files;
|
|
11
|
+
if (files && files.length > 0) {
|
|
12
|
+
const path = files[0].webkitRelativePath.split("/")[0];
|
|
13
|
+
setDirectoryPath(path);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className={className}>
|
|
19
|
+
<input
|
|
20
|
+
type="file"
|
|
21
|
+
// dir=""
|
|
22
|
+
onChange={handleDirectorySelect}
|
|
23
|
+
style={{display: "none"}}
|
|
24
|
+
id="directory-input"
|
|
25
|
+
/>
|
|
26
|
+
<label htmlFor="directory-input">
|
|
27
|
+
<Button variant="contained" component="span">
|
|
28
|
+
Select Directory
|
|
29
|
+
</Button>
|
|
30
|
+
</label>
|
|
31
|
+
<TextField
|
|
32
|
+
label="Selected Directory"
|
|
33
|
+
variant="outlined"
|
|
34
|
+
fullWidth
|
|
35
|
+
value={directoryPath}
|
|
36
|
+
InputProps={{readOnly: true}}
|
|
37
|
+
sx={{mt: 2}}
|
|
38
|
+
{...rest}
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default DirectoryPicker;
|