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,505 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import _every from "lodash/every";
|
|
3
|
+
import _filter from "lodash/filter";
|
|
4
|
+
import _find from "lodash/find";
|
|
5
|
+
import _first from "lodash/first";
|
|
6
|
+
import _get from "lodash/get";
|
|
7
|
+
import _includes from "lodash/includes";
|
|
8
|
+
import _isArray from "lodash/isArray";
|
|
9
|
+
import _isEmpty from "lodash/isEmpty";
|
|
10
|
+
import _isNaN from "lodash/isNaN";
|
|
11
|
+
import _isNil from "lodash/isNil";
|
|
12
|
+
import _map from "lodash/map";
|
|
13
|
+
import _min from "lodash/min";
|
|
14
|
+
import _pull from "lodash/pull";
|
|
15
|
+
import _reduce from "lodash/reduce";
|
|
16
|
+
import _replace from "lodash/replace";
|
|
17
|
+
import _size from "lodash/size";
|
|
18
|
+
import _some from "lodash/some";
|
|
19
|
+
import _sumBy from "lodash/sumBy";
|
|
20
|
+
import _template from "lodash/template";
|
|
21
|
+
import _times from "lodash/times";
|
|
22
|
+
import _toString from "lodash/toString";
|
|
23
|
+
import moment from "moment";
|
|
24
|
+
import path from "path";
|
|
25
|
+
import React, {useCallback, useEffect, useRef, useState} from "react";
|
|
26
|
+
import {useTranslation} from "react-i18next";
|
|
27
|
+
import {useDebounceValue} from "usehooks-ts";
|
|
28
|
+
import YTDlpWrap, {Progress as YtDlpProgress} from "yt-dlp-wrap";
|
|
29
|
+
|
|
30
|
+
import {Alert, Box, CircularProgress} from "@mui/material";
|
|
31
|
+
import Grid from "@mui/material/Grid2";
|
|
32
|
+
|
|
33
|
+
import StoreSchema, {ApplicationOptions} from "../../common/Store";
|
|
34
|
+
import {AlbumInfo, TrackInfo, TrackStatusInfo} from "../../common/Youtube";
|
|
35
|
+
import FormatSelector, {Format} from "../../components/youtube/formatSelector/FormatSelector";
|
|
36
|
+
import InputPanel from "../../components/youtube/inputPanel/InputPanel";
|
|
37
|
+
import MediaInfoPanel from "../../components/youtube/mediaInfoPanel/MediaInfoPanel";
|
|
38
|
+
import TrackList from "../../components/youtube/trackList/TrackList";
|
|
39
|
+
import {MediaFormat} from "../../enums/Media";
|
|
40
|
+
import {useAppContext} from "../../react/contexts/AppContext";
|
|
41
|
+
import {useDataState} from "../../react/contexts/DataContext";
|
|
42
|
+
// import CompleteTracksMock from "../../tests/CompleteTracksMock";
|
|
43
|
+
import MissingDetailsTracksMock from "../../tests/MissingDetailsTracksMock";
|
|
44
|
+
import Styles from "./HomeView.styl";
|
|
45
|
+
|
|
46
|
+
const isDev = process.env.NODE_ENV === "development";
|
|
47
|
+
const binPath = _replace(!isDev ? path.join(process.resourcesPath, "bin") : path.join(__dirname, "resources", "bin"), /\\/g, "/");
|
|
48
|
+
const ytDlpWrap = new YTDlpWrap(binPath + "/yt-dlp.exe");
|
|
49
|
+
|
|
50
|
+
const abortControllersRef: {[key: string]: AbortController} = {};
|
|
51
|
+
|
|
52
|
+
export const HomeView: React.FC = () => {
|
|
53
|
+
const [appOptions, setAppOptions] = useState<ApplicationOptions>(global.store.get("application"));
|
|
54
|
+
const {t} = useTranslation();
|
|
55
|
+
const {album, tracks, trackStatus, trackCuts, setAlbum, setTracks, setTrackStatus, clear} = useDataState();
|
|
56
|
+
const {state, actions} = useAppContext();
|
|
57
|
+
const [error, setError] = useState(false);
|
|
58
|
+
const [downloadStart, setDownloadStart] = useState(false);
|
|
59
|
+
const trackStatusRef = useRef<TrackStatusInfo[]>(trackStatus);
|
|
60
|
+
const queueRef = useRef<string[]>(state.queue);
|
|
61
|
+
const [debouncedAppOptions] = useDebounceValue(appOptions, 500, {leading: true});
|
|
62
|
+
|
|
63
|
+
const onFormatSelected = (value: Format) => {
|
|
64
|
+
setAppOptions((prev) => ({...prev, format: value}));
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
global.store.set("application", debouncedAppOptions);
|
|
69
|
+
}, [debouncedAppOptions]);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!downloadStart || !appOptions.format) return;
|
|
73
|
+
|
|
74
|
+
downloadAlbum();
|
|
75
|
+
}, [appOptions.format, downloadStart]);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
trackStatusRef.current = trackStatus;
|
|
79
|
+
}, [trackStatus]);
|
|
80
|
+
|
|
81
|
+
const download = async (url: string) => {
|
|
82
|
+
if (!album) {
|
|
83
|
+
const result = await loadInfo(url);
|
|
84
|
+
|
|
85
|
+
if (_some(result, v => _isNil(v))) {
|
|
86
|
+
setError(true);
|
|
87
|
+
} else {
|
|
88
|
+
setDownloadStart(true);
|
|
89
|
+
}
|
|
90
|
+
} else if (_some(album, v => _isNil(v))) {
|
|
91
|
+
setError(true);
|
|
92
|
+
} else {
|
|
93
|
+
setError(false);
|
|
94
|
+
setDownloadStart(true);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const handleUrlChange = (url: string) => {
|
|
99
|
+
setAppOptions((prev) => ({...prev, url}));
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const onCancel = () => {
|
|
103
|
+
_map(abortControllersRef, (v) => v.abort());
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const cancelTrack = (id: string) => {
|
|
107
|
+
const controller = abortControllersRef[id];
|
|
108
|
+
controller.abort();
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const getResolveDataPromise = (url: string): Promise<any> => {
|
|
112
|
+
if (appOptions.debugMode) {
|
|
113
|
+
return new Promise<any>((resolve) => {
|
|
114
|
+
setTimeout(() => resolve(MissingDetailsTracksMock), 1000);
|
|
115
|
+
});
|
|
116
|
+
} else {
|
|
117
|
+
return ytDlpWrap.getVideoInfo(url);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const resolveData = (url: string) => {
|
|
122
|
+
const promise = getResolveDataPromise(url);
|
|
123
|
+
promise.finally(() => _pull(state.queue, "resolve-data"));
|
|
124
|
+
|
|
125
|
+
queueRef.current.push("resolve-data");
|
|
126
|
+
|
|
127
|
+
return promise;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const loadInfo = useCallback(async (url: string) => {
|
|
131
|
+
try {
|
|
132
|
+
clearMedia();
|
|
133
|
+
actions.setLoading(true);
|
|
134
|
+
const info = await resolveData(url);
|
|
135
|
+
const items: TrackInfo[] = _isArray(info) ? info : [info];
|
|
136
|
+
const albumInfo = getAlbumInfo(items);
|
|
137
|
+
|
|
138
|
+
setTracks(items);
|
|
139
|
+
setAlbum(albumInfo);
|
|
140
|
+
actions.setLoading(false);
|
|
141
|
+
|
|
142
|
+
return albumInfo;
|
|
143
|
+
} catch {
|
|
144
|
+
actions.setLoading(false);
|
|
145
|
+
}
|
|
146
|
+
}, [setTracks, setAlbum]);
|
|
147
|
+
|
|
148
|
+
const mapRange = (x: number, inRange: [number, number], outRange: number[]) => {
|
|
149
|
+
return ((x - inRange[0]) * (outRange[1] - outRange[0])) / (inRange[1] - inRange[0]) + outRange[0];
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const isAlbumTrack = (track: TrackInfo) => {
|
|
153
|
+
return !!track.playlist;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const setProgressPercentage = useCallback((trackId: string, value?: number) => {
|
|
157
|
+
setTrackStatus((prev) => _map(prev, (item) => {
|
|
158
|
+
if (item.trackId === trackId) {
|
|
159
|
+
return {...item, percent: _isNaN(value) ? item.percent : value};
|
|
160
|
+
} else {
|
|
161
|
+
return item;
|
|
162
|
+
}
|
|
163
|
+
}));
|
|
164
|
+
}, [trackStatus, setTrackStatus]);
|
|
165
|
+
|
|
166
|
+
const updateProgress = useCallback((trackId: string, progress: YtDlpProgress, progressMapRange = [0, 100]) => {
|
|
167
|
+
setProgressPercentage(trackId, mapRange(progress.percent, [0, 100], progressMapRange));
|
|
168
|
+
}, [trackStatus, setTrackStatus]);
|
|
169
|
+
|
|
170
|
+
const updateProgressStatus = useCallback((trackId: string, eventType: string) => {
|
|
171
|
+
let status = "";
|
|
172
|
+
|
|
173
|
+
if (eventType === "youtube") {
|
|
174
|
+
status = t("reading");
|
|
175
|
+
setProgressPercentage(trackId, 5);
|
|
176
|
+
} else if (eventType === "info") {
|
|
177
|
+
status = t("startingDownload");
|
|
178
|
+
setProgressPercentage(trackId, 10);
|
|
179
|
+
} else if (eventType === "download") {
|
|
180
|
+
status = t("downloading");
|
|
181
|
+
} else if (eventType === "ExtractAudio") {
|
|
182
|
+
status = t("extractingAudio");
|
|
183
|
+
setProgressPercentage(trackId, 90);
|
|
184
|
+
} else if (eventType === "Merger") {
|
|
185
|
+
status = t("merging");
|
|
186
|
+
setProgressPercentage(trackId, 85);
|
|
187
|
+
} else if (eventType === "convertingThumbnail") {
|
|
188
|
+
setProgressPercentage(trackId, 90);
|
|
189
|
+
} else if (eventType === "embeddingThumbnail") {
|
|
190
|
+
setProgressPercentage(trackId, 95);
|
|
191
|
+
} else {
|
|
192
|
+
status = "";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
setTrackStatus((prev) => _map(prev, (item) => {
|
|
196
|
+
if (item.trackId === trackId) {
|
|
197
|
+
return {...item, status};
|
|
198
|
+
} else {
|
|
199
|
+
return item;
|
|
200
|
+
}
|
|
201
|
+
}));
|
|
202
|
+
}, [trackStatus, setTrackStatus]);
|
|
203
|
+
|
|
204
|
+
const isQueueCompleted = () => {
|
|
205
|
+
return _every(queueRef.current, (item) => {
|
|
206
|
+
const status = _find(trackStatusRef.current, ["trackId", item]);
|
|
207
|
+
|
|
208
|
+
return (!status && !abortControllersRef[item]) || (status && (status.completed || status.error || !abortControllersRef[status.trackId]));
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const onProcessEnd = useCallback((result: {trackId: string, error?: string}) => {
|
|
213
|
+
const controller = abortControllersRef[result.trackId];
|
|
214
|
+
const aborted = controller?.signal.aborted;
|
|
215
|
+
const nextTrackToDownload = _find(queueRef.current, (item) => !_find(trackStatusRef.current, (status) => status.trackId === item));
|
|
216
|
+
|
|
217
|
+
const track = _find(tracks, ["id", result.trackId]);
|
|
218
|
+
const outputPath = getOutputFilePath(track, album);
|
|
219
|
+
const totalSize = fs.existsSync(outputPath) ? fs.statSync(outputPath).size : 0;
|
|
220
|
+
|
|
221
|
+
if (result.error && _includes(result.error, "[generic] '' is not a valid URL")) {
|
|
222
|
+
result.error = undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
setTrackStatus((prev) => _map(prev, (item) => {
|
|
226
|
+
if (item.trackId === result.trackId) {
|
|
227
|
+
return {
|
|
228
|
+
...item,
|
|
229
|
+
status: aborted ? t("cancelled") : result.error ?? t("done"),
|
|
230
|
+
error: !!result.error,
|
|
231
|
+
completed: !result.error,
|
|
232
|
+
percent: 100,
|
|
233
|
+
totalSize,
|
|
234
|
+
};
|
|
235
|
+
} else {
|
|
236
|
+
return item;
|
|
237
|
+
}
|
|
238
|
+
}));
|
|
239
|
+
|
|
240
|
+
delete abortControllersRef[result.trackId];
|
|
241
|
+
|
|
242
|
+
if (isQueueCompleted()) {
|
|
243
|
+
queueRef.current = [];
|
|
244
|
+
actions.setQueue([]);
|
|
245
|
+
setDownloadStart(false);
|
|
246
|
+
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (aborted || !nextTrackToDownload) {
|
|
250
|
+
setDownloadStart(false);
|
|
251
|
+
} else {
|
|
252
|
+
downloadTrack(nextTrackToDownload);
|
|
253
|
+
}
|
|
254
|
+
}, [tracks, trackStatus, trackStatusRef.current, queueRef.current, appOptions]);
|
|
255
|
+
|
|
256
|
+
const downloadAlbum = () => {
|
|
257
|
+
setTrackStatus([]);
|
|
258
|
+
setDownloadStart(false);
|
|
259
|
+
queueRef.current = _map(tracks, "id");
|
|
260
|
+
|
|
261
|
+
_times(_min([appOptions.concurrency, _size(tracks)]), (num) => {
|
|
262
|
+
const id = tracks[num].id;
|
|
263
|
+
|
|
264
|
+
trackStatusRef.current = _map(trackStatusRef.current, (item) => ({...item, percent: 0}));
|
|
265
|
+
setTrackStatus((prev) => _map(prev, (item) => ({...item, percent: 0})));
|
|
266
|
+
|
|
267
|
+
downloadTrack(id);
|
|
268
|
+
});
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const downloadFailed = () => {
|
|
272
|
+
const failedTracks = _filter(trackStatusRef.current, "error");
|
|
273
|
+
|
|
274
|
+
queueRef.current = _map(failedTracks, "trackId");
|
|
275
|
+
setTrackStatus((prev) => _filter(prev, (p) => !p.error));
|
|
276
|
+
trackStatusRef.current = _filter(trackStatusRef.current, (p) => !p.error);
|
|
277
|
+
|
|
278
|
+
for (const failed of failedTracks) {
|
|
279
|
+
downloadTrack(failed.trackId);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const clearMedia = () => {
|
|
284
|
+
clear();
|
|
285
|
+
onCancel();
|
|
286
|
+
actions.setQueue([]);
|
|
287
|
+
setDownloadStart(false);
|
|
288
|
+
setAppOptions((prev) => ({...prev, format: {}}));
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const handleClear = () => {
|
|
292
|
+
setAppOptions((prev) => ({...prev, url: ""}));
|
|
293
|
+
clearMedia();
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const getYtDplArguments = (track: TrackInfo, album: AlbumInfo) => {
|
|
297
|
+
if (appOptions.format.type === MediaFormat.Audio) {
|
|
298
|
+
return [
|
|
299
|
+
"--extract-audio",
|
|
300
|
+
"--audio-format", appOptions.format.extension,
|
|
301
|
+
appOptions.format.extension !== "wav" ? "--embed-thumbnail" : "", // wav does not support thumbnail embedding
|
|
302
|
+
"--audio-quality", _toString(appOptions.format.audioQuality),
|
|
303
|
+
...getCutArgs(track),
|
|
304
|
+
"--postprocessor-args", getPostProcessorArgs(track, album),
|
|
305
|
+
appOptions.alwaysOverwrite ? "--force-alwaysOverwrites" : "",
|
|
306
|
+
"--output", getOutput(track, album)
|
|
307
|
+
];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (appOptions.format.type === MediaFormat.Video) {
|
|
311
|
+
const selected = appOptions.format.videoQuality;
|
|
312
|
+
const [, height] = _map(selected.match(/\d+/g), Number);
|
|
313
|
+
const ext = appOptions.format.extension === "mkv" ? "webm" : appOptions.format.extension;
|
|
314
|
+
|
|
315
|
+
return [
|
|
316
|
+
"-f", `bv*[height<=${height}][ext=${ext}]+ba[ext=m4a]/b[height<=${height}][ext=${ext}] / bv*+ba/b`,
|
|
317
|
+
...getCutArgs(track),
|
|
318
|
+
"--embed-thumbnail",
|
|
319
|
+
appOptions.alwaysOverwrite ? "--force-alwaysOverwrites" : "",
|
|
320
|
+
"--postprocessor-args", getPostProcessorArgs(track, album),
|
|
321
|
+
"--output", getOutput(track, album)
|
|
322
|
+
];
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const getCutsPostProcessorArgs = (track: TrackInfo) => {
|
|
327
|
+
const cuts = trackCuts[track.id];
|
|
328
|
+
if (_isEmpty(cuts)) {
|
|
329
|
+
return "";
|
|
330
|
+
} else {
|
|
331
|
+
const start = moment.duration(cuts[0], "seconds");
|
|
332
|
+
const end = moment.duration(cuts[1], "seconds");
|
|
333
|
+
const length = end.subtract(start);
|
|
334
|
+
|
|
335
|
+
const t = length.format("H:m:s");
|
|
336
|
+
|
|
337
|
+
return `-ss 0:0:0 -to ${t} `;
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const getPostProcessorArgs = (track: TrackInfo, album: AlbumInfo) => {
|
|
342
|
+
if (isAlbumTrack(track)) {
|
|
343
|
+
const title = track.title.replace(/"/g, "\\\"");
|
|
344
|
+
const artist = album.artist.replace(/"/g, "\\\"");
|
|
345
|
+
const albumTitle = album.title.replace(/"/g, "\\\"");
|
|
346
|
+
return getCutsPostProcessorArgs(track) + `-metadata title="${title}" -metadata artist="${artist}" -metadata album="${albumTitle}" -metadata track="${track.playlist_autonumber}" -metadata date="${album.releaseYear}" -metadata release_year="${album.releaseYear}"`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return getCutsPostProcessorArgs(track) + `-metadata title="${track.title}"`;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const getCutArgs = (track: TrackInfo): string[] => {
|
|
353
|
+
const cuts = trackCuts[track.id];
|
|
354
|
+
|
|
355
|
+
if (_isEmpty(cuts)) {
|
|
356
|
+
return [];
|
|
357
|
+
} else {
|
|
358
|
+
return ["--download-sections", `*${moment.duration(cuts[0], "seconds").format("HH:mm:ss")}-${moment.duration(cuts[1], "seconds").format("HH:mm:ss")}`, "-S", "proto:https"];
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const getOutput = (track: TrackInfo, album: AlbumInfo) => {
|
|
363
|
+
return getOutputFile(track, album) + ".%(ext)s";
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const getOutputFilePath = (track: TrackInfo, album: AlbumInfo) => {
|
|
367
|
+
return getOutputFile(track, album) + "." + appOptions.format.extension;
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const getOutputFile = (track: TrackInfo, album: AlbumInfo) => {
|
|
371
|
+
const interpolate = /{{([\s\S]+?)}}/g;
|
|
372
|
+
const data = {
|
|
373
|
+
albumTitle: album.title,
|
|
374
|
+
artist: album.artist,
|
|
375
|
+
trackTitle: track.title,
|
|
376
|
+
trackNo: track.playlist_autonumber,
|
|
377
|
+
releaseYear: album.releaseYear
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
if (appOptions.format.type === MediaFormat.Audio) {
|
|
381
|
+
if (!isAlbumTrack(track)) {
|
|
382
|
+
try {
|
|
383
|
+
const compiled = _template(appOptions.trackOutputTemplate, {interpolate});
|
|
384
|
+
|
|
385
|
+
return `${appOptions.outputDirectory}/${compiled(data)}`;
|
|
386
|
+
} catch {
|
|
387
|
+
const defaultTrackOutputTemplate = _get(StoreSchema.application, "properties.trackOutputTemplate.default");
|
|
388
|
+
const compiled = _template(defaultTrackOutputTemplate, {interpolate});
|
|
389
|
+
|
|
390
|
+
return `${appOptions.outputDirectory}/${compiled(data)}`;
|
|
391
|
+
}
|
|
392
|
+
} else {
|
|
393
|
+
try {
|
|
394
|
+
const compiled = _template(appOptions.albumOutputTemplate, {interpolate});
|
|
395
|
+
|
|
396
|
+
return `${appOptions.outputDirectory}/${compiled(data)}`;
|
|
397
|
+
} catch {
|
|
398
|
+
const defaultAlbumOutputTemplate = _get(StoreSchema.application, "properties.albumOutputTemplate.default");
|
|
399
|
+
const compiled = _template(defaultAlbumOutputTemplate, {interpolate});
|
|
400
|
+
|
|
401
|
+
return `${appOptions.outputDirectory}/${compiled(data)}`;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (appOptions.format.type === MediaFormat.Video) {
|
|
407
|
+
if (!isAlbumTrack(track)) {
|
|
408
|
+
try {
|
|
409
|
+
const compiled = _template(appOptions.videoOutputTemplate, {interpolate});
|
|
410
|
+
|
|
411
|
+
return `${appOptions.outputDirectory}/${compiled(data)}`;
|
|
412
|
+
} catch {
|
|
413
|
+
const defaultVideoOutputTemplate = _get(StoreSchema.application, "properties.videoOutputTemplate.default");
|
|
414
|
+
const compiled = _template(defaultVideoOutputTemplate, {interpolate});
|
|
415
|
+
|
|
416
|
+
return `${appOptions.outputDirectory}/${compiled(data)}`;
|
|
417
|
+
}
|
|
418
|
+
} else {
|
|
419
|
+
try {
|
|
420
|
+
const compiled = _template(appOptions.playlistOutputTemplate, {interpolate});
|
|
421
|
+
|
|
422
|
+
return `${appOptions.outputDirectory}/${compiled(data)}`;
|
|
423
|
+
} catch {
|
|
424
|
+
const defaultPlaylistOutputTemplate = _get(StoreSchema.application, "properties.playlistOutputTemplate.default");
|
|
425
|
+
const compiled = _template(defaultPlaylistOutputTemplate, {interpolate});
|
|
426
|
+
|
|
427
|
+
return `${appOptions.outputDirectory}/${compiled(data)}`;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const downloadTrack = (trackId: string) => {
|
|
434
|
+
const track = _find(tracks, ["id", trackId]);
|
|
435
|
+
const controller = new AbortController();
|
|
436
|
+
const newTrackProgressInfo = {
|
|
437
|
+
trackId: track.id,
|
|
438
|
+
percent: 0,
|
|
439
|
+
totalSize: track.filesize_approx,
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
setTrackStatus((prev) => [...prev, newTrackProgressInfo]);
|
|
443
|
+
|
|
444
|
+
if (!_includes(queueRef.current, trackId)) {
|
|
445
|
+
queueRef.current.push(trackId);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
abortControllersRef[trackId] = controller;
|
|
449
|
+
|
|
450
|
+
const proc = ytDlpWrap.exec([track.original_url, ...getYtDplArguments(track, album)], {shell: false, windowsHide: false, detached: false}, controller.signal)
|
|
451
|
+
.on("progress", (progress) => updateProgress(track.id, progress, appOptions.format.type === MediaFormat.Audio ? [10, 90] : [10, 85]))
|
|
452
|
+
.on("ytDlpEvent", (eventType) => updateProgressStatus(track.id, eventType))
|
|
453
|
+
.on("error", (error) => onProcessEnd({trackId: track.id, error: error.message}))
|
|
454
|
+
.on("close", () => {
|
|
455
|
+
onProcessEnd({trackId: track.id});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
console.log(proc.ytDlpProcess.pid);
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
const getAlbumInfo = (items: TrackInfo[]): AlbumInfo => {
|
|
462
|
+
const item = _first(items);
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
artist: _get(item, "creators.0", _get(item, "artist", item.channel)),
|
|
466
|
+
title: isAlbumTrack(item) ? _get(item, "album", _get(item, "playlist_title", _get(item, "playlist"))) : item.title,
|
|
467
|
+
releaseYear: _get(item, "release_year") ?? (new Date(item.timestamp * 1000)).getFullYear(),
|
|
468
|
+
tracksNumber: _get(item, "playlist_count", 1),
|
|
469
|
+
duration: _sumBy(items, "duration"),
|
|
470
|
+
thumbnail: _get(item, "thumbnail", _get(_find(item.thumbnails, ["id", "2"]), "url")),
|
|
471
|
+
};
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const getTotalProgress = () => {
|
|
475
|
+
const total = _size(queueRef.current) * 100;
|
|
476
|
+
const progress = _reduce(queueRef.current, (prev: number, curr: string) => {
|
|
477
|
+
const trackStatus = _find(trackStatusRef.current, ["trackId", curr]);
|
|
478
|
+
|
|
479
|
+
return trackStatus ? prev + trackStatus.percent : prev;
|
|
480
|
+
}, 0);
|
|
481
|
+
|
|
482
|
+
return progress / total * 100;
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
return (
|
|
486
|
+
<Box className={Styles.home}>
|
|
487
|
+
<div className={Styles.header}>
|
|
488
|
+
<InputPanel value={appOptions.url} onChange={handleUrlChange} loading={state.loading || !_isEmpty(queueRef.current)} onDownload={download} onDownloadFailed={downloadFailed} onLoadInfo={loadInfo} onClear={handleClear} />
|
|
489
|
+
{!_isEmpty(tracks) && <FormatSelector value={appOptions.format} onSelected={onFormatSelected} />}
|
|
490
|
+
</div>
|
|
491
|
+
<Grid className={Styles.content} container spacing={2} padding={2}>
|
|
492
|
+
{state.loading && <CircularProgress color="primary" thickness={5} size={80} />}
|
|
493
|
+
{!state.loading && tracks && album &&
|
|
494
|
+
<>
|
|
495
|
+
{error && <Alert className={Styles.error} severity="error">{t("missingMediaInfoError")}</Alert>}
|
|
496
|
+
<MediaInfoPanel loading={!_isEmpty(queueRef.current)} progress={getTotalProgress()} onCancel={onCancel} />
|
|
497
|
+
<TrackList queue={queueRef.current} onDownloadTrack={downloadTrack} onCancelTrack={cancelTrack}/>
|
|
498
|
+
</>
|
|
499
|
+
}
|
|
500
|
+
</Grid>
|
|
501
|
+
</Box>
|
|
502
|
+
);
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
export default HomeView;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
@import "../../styles/mixins.styl"
|
|
2
|
+
@import "../../styles/fonts.styl"
|
|
3
|
+
|
|
4
|
+
.settings {
|
|
5
|
+
flex-grow: 1;
|
|
6
|
+
|
|
7
|
+
.container {
|
|
8
|
+
height: 100%;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
|
|
11
|
+
.content {
|
|
12
|
+
flex-grow: 1;
|
|
13
|
+
align-content: flex-start;
|
|
14
|
+
|
|
15
|
+
.group {
|
|
16
|
+
padding: 1rem;
|
|
17
|
+
align-content: flex-start;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.footer {
|
|
22
|
+
flex-grow: 0;
|
|
23
|
+
justify-content: center;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|