yt-grabber 1.7.0 → 1.8.1
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 +3 -1
- package/src/automations/YoutubeAlbums.ts +7 -0
- package/src/automations/YoutubeArtists.ts +11 -7
- package/src/automations/YoutubeTracks.ts +7 -0
- package/src/common/Helpers.ts +19 -9
- package/src/common/Store.ts +5 -0
- package/src/components/progress/Progress.styl +11 -0
- package/src/components/progress/Progress.tsx +3 -2
- package/src/components/youtube/playlistTabs/PlaylistTabs.tsx +3 -0
- package/src/resources/bin/yt-dlp.exe +0 -0
- package/src/resources/locales/de-DE/help.json +5 -1
- package/src/resources/locales/de-DE/translation.json +3 -1
- package/src/resources/locales/en-GB/help.json +5 -1
- package/src/resources/locales/en-GB/translation.json +3 -1
- package/src/resources/locales/pl-PL/help.json +5 -1
- package/src/resources/locales/pl-PL/translation.json +3 -1
- package/src/views/development/DevelopmentView.tsx +1 -14
- package/src/views/home/HomeView.tsx +7 -6
- package/src/views/settings/SettingsView.styl +4 -0
- package/src/views/settings/SettingsView.tsx +55 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yt-grabber",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"description": "Youtube Grabber",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"repository": {
|
|
@@ -96,6 +96,7 @@
|
|
|
96
96
|
"react-number-format": "^5.4.3",
|
|
97
97
|
"react-router-dom": "^7.4.0",
|
|
98
98
|
"usehooks-ts": "^3.1.1",
|
|
99
|
+
"win-version-info": "^6.0.1",
|
|
99
100
|
"yt-dlp-wrap": "^2.3.12"
|
|
100
101
|
},
|
|
101
102
|
"devDependencies": {
|
|
@@ -115,6 +116,7 @@
|
|
|
115
116
|
"@types/react-dom": "^19.0.4",
|
|
116
117
|
"@types/webpack-dev-server": "^4.7.2",
|
|
117
118
|
"@types/webpack-env": "^1.18.8",
|
|
119
|
+
"@types/win-version-info": "^3.1.3",
|
|
118
120
|
"@typescript-eslint/eslint-plugin": "^8.28.0",
|
|
119
121
|
"@typescript-eslint/parser": "^8.28.0",
|
|
120
122
|
"concurrently": "^9.1.2",
|
|
@@ -71,8 +71,15 @@ const run = async (params: GetYoutubeParams, options: LaunchOptions, i18n: i18ne
|
|
|
71
71
|
|
|
72
72
|
const process = async (album: string) => {
|
|
73
73
|
const results: string[] = [];
|
|
74
|
+
const albumUrlRegex = /^https?:\/\/.*playlist/i;
|
|
74
75
|
|
|
75
76
|
try {
|
|
77
|
+
if (albumUrlRegex.test(album)) {
|
|
78
|
+
results.push(album);
|
|
79
|
+
|
|
80
|
+
return results;
|
|
81
|
+
}
|
|
82
|
+
|
|
76
83
|
const searchInput = await page.waitForSelector(`::-p-xpath(${YtMusicSearchInputSelector})`, {timeout: 1000});
|
|
77
84
|
await clearInput(searchInput, page);
|
|
78
85
|
await searchInput.type(album);
|
|
@@ -85,13 +85,6 @@ const run = async (
|
|
|
85
85
|
|
|
86
86
|
const process = async (artist: string) => {
|
|
87
87
|
const results: string[] = [];
|
|
88
|
-
const searchInput = await page.waitForSelector(`::-p-xpath(${YtMusicSearchInputSelector})`, {timeout: 1000});
|
|
89
|
-
|
|
90
|
-
await clearInput(searchInput, page);
|
|
91
|
-
await searchInput.type(artist);
|
|
92
|
-
page.keyboard.press("Enter");
|
|
93
|
-
await page.waitForNetworkIdle();
|
|
94
|
-
|
|
95
88
|
const artistChannelUrl = await getArtistUrl(params, artist, onPause);
|
|
96
89
|
|
|
97
90
|
await navigateToPage(artistChannelUrl, page);
|
|
@@ -120,6 +113,17 @@ const run = async (
|
|
|
120
113
|
|
|
121
114
|
const getArtistUrl = async (params: GetYoutubeParams, artist: string, onPause?: (data: YoutubeArtist[]) => Promise<YoutubeArtist>): Promise<string> => {
|
|
122
115
|
try {
|
|
116
|
+
const searchInput = await page.waitForSelector(`::-p-xpath(${YtMusicSearchInputSelector})`, {timeout: 1000});
|
|
117
|
+
const channelUrlRegex = /^https?:\/\/.*channel/i;
|
|
118
|
+
|
|
119
|
+
if (channelUrlRegex.test(artist)) {
|
|
120
|
+
return artist;
|
|
121
|
+
} else {
|
|
122
|
+
await clearInput(searchInput, page);
|
|
123
|
+
await searchInput.type(artist);
|
|
124
|
+
page.keyboard.press("Enter");
|
|
125
|
+
await page.waitForNetworkIdle();
|
|
126
|
+
}
|
|
123
127
|
const artistsChip = await page.waitForSelector(`::-p-xpath(${YtMusicArtistsChipSelector})`, {visible: true, timeout: 1000});
|
|
124
128
|
|
|
125
129
|
artistsChip.click();
|
|
@@ -72,8 +72,15 @@ const run = async (params: GetYoutubeParams, options: LaunchOptions, i18n: i18ne
|
|
|
72
72
|
|
|
73
73
|
const process = async (song: string) => {
|
|
74
74
|
const results: string[] = [];
|
|
75
|
+
const trackUrlRegex = /^https?:\/\/.*watch/i;
|
|
75
76
|
|
|
76
77
|
try {
|
|
78
|
+
if (trackUrlRegex.test(song)) {
|
|
79
|
+
results.push(song);
|
|
80
|
+
|
|
81
|
+
return results;
|
|
82
|
+
}
|
|
83
|
+
|
|
77
84
|
const searchInput = await page.waitForSelector(`::-p-xpath(${YtMusicSearchInputSelector})`, {timeout: 1000});
|
|
78
85
|
await clearInput(searchInput, page);
|
|
79
86
|
await searchInput.type(song);
|
package/src/common/Helpers.ts
CHANGED
|
@@ -43,20 +43,30 @@ export const resolveMockData = (delay = 1000) => {
|
|
|
43
43
|
|
|
44
44
|
export const waitFor = (miliseconds: number) => new Promise((resolve) => setTimeout(resolve, miliseconds));
|
|
45
45
|
|
|
46
|
-
export const
|
|
47
|
-
const artistRegex = /^(?:https?:\/\/)?(?:www\.)?(?:m\.)?(?:music\.)?(?:youtube\.com\/|youtu\.be\/)?(channel)/;
|
|
46
|
+
export const isPlaylist = (url: string) => {
|
|
48
47
|
const playlistRegex = /^(?:https?:\/\/)?(?:www\.)?(?:music\.)?youtube\.com\/(?:playlist\?list=|watch\?.*?\blist=)([a-zA-Z0-9_-]+)/;
|
|
48
|
+
|
|
49
|
+
return playlistRegex.test(url);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const isArtist = (url: string) => {
|
|
53
|
+
const artistRegex = /^(?:https?:\/\/)?(?:www\.)?(?:m\.)?(?:music\.)?(?:youtube\.com\/|youtu\.be\/)?(channel)/;
|
|
54
|
+
|
|
55
|
+
return artistRegex.test(url);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const isTrack = (url: string) => {
|
|
49
59
|
const trackRegex = /^(?:https?:\/\/)?(?:www\.)?(?:music\.)?youtube\.com\/watch\?.*?\bv=([a-zA-Z0-9_-]+)/;
|
|
50
60
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (isArtist) {
|
|
61
|
+
return trackRegex.test(url);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const getUrlType = (url: string) => {
|
|
65
|
+
if (isArtist(url)) {
|
|
56
66
|
return UrlType.Artist;
|
|
57
|
-
} else if (isPlaylist) {
|
|
67
|
+
} else if (isPlaylist(url)) {
|
|
58
68
|
return UrlType.Playlist;
|
|
59
|
-
} else if (isTrack) {
|
|
69
|
+
} else if (isTrack(url)) {
|
|
60
70
|
return UrlType.Track;
|
|
61
71
|
}
|
|
62
72
|
|
package/src/common/Store.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type ApplicationOptions = {
|
|
|
8
8
|
outputDirectory?: string;
|
|
9
9
|
ytdlpExecutablePath?: string;
|
|
10
10
|
ffmpegExecutablePath?: string;
|
|
11
|
+
chromeExecutablePath?: string;
|
|
11
12
|
albumOutputTemplate?: string;
|
|
12
13
|
playlistOutputTemplate?: string;
|
|
13
14
|
videoOutputTemplate?: string;
|
|
@@ -72,6 +73,10 @@ export const StoreSchema: Schema<IStore> = {
|
|
|
72
73
|
type: "string",
|
|
73
74
|
default: ""
|
|
74
75
|
},
|
|
76
|
+
chromeExecutablePath: {
|
|
77
|
+
type: "string",
|
|
78
|
+
default: ""
|
|
79
|
+
},
|
|
75
80
|
albumOutputTemplate: {
|
|
76
81
|
type: "string",
|
|
77
82
|
default: "{{artist}}/[{{releaseYear}}] {{albumTitle}}/{{trackNo}} - {{trackTitle}}",
|
|
@@ -9,13 +9,14 @@ export type ProgressProps = CircularProgressProps & {
|
|
|
9
9
|
labelScale?: number;
|
|
10
10
|
label?: boolean;
|
|
11
11
|
renderLabel?: (value: number) => React.ReactNode;
|
|
12
|
+
position?: "absolute" | "inline";
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
export const Progress: React.FC<ProgressProps> = (props) => {
|
|
15
|
-
const {size, labelScale = 1, label = true, renderLabel, className, ...rest} = props;
|
|
16
|
+
const {size, labelScale = 1, label = true, renderLabel, position = "inline", className, ...rest} = props;
|
|
16
17
|
|
|
17
18
|
return (
|
|
18
|
-
<Box className={classnames(Styles.progress, className)}>
|
|
19
|
+
<Box className={classnames(Styles.progress, className, Styles[position])}>
|
|
19
20
|
<CircularProgress variant="determinate" size={size} {...rest} />
|
|
20
21
|
{label && <Box className={Styles.labelWrapper}>
|
|
21
22
|
<Typography variant="caption" sx={{scale: labelScale}}>{renderLabel ? renderLabel(props.value) : `${Math.round(props.value)}%`}</Typography>
|
|
@@ -15,6 +15,7 @@ import _size from "lodash/size";
|
|
|
15
15
|
import _some from "lodash/some";
|
|
16
16
|
import path from "path";
|
|
17
17
|
import React, {MouseEvent, useCallback, useEffect} from "react";
|
|
18
|
+
import {useTranslation} from "react-i18next";
|
|
18
19
|
|
|
19
20
|
import CloseIcon from "@mui/icons-material/Close";
|
|
20
21
|
import TabContext from "@mui/lab/TabContext";
|
|
@@ -47,6 +48,7 @@ export const PlaylistTabs: React.FC<PlaylistTabsProps> = (props: PlaylistTabsPro
|
|
|
47
48
|
const {queue, onDownloadTrack, onDownloadPlaylist, onCancelPlaylist, onCancelTrack} = props;
|
|
48
49
|
const {trackStatus, playlists, activeTab, setActiveTab, setTrackStatus, setPlaylists, setTracks} = useDataState();
|
|
49
50
|
const {state} = useAppContext();
|
|
51
|
+
const {t} = useTranslation();
|
|
50
52
|
const tabWidth = window.innerWidth / (playlists.length + playlists.length) - 30;
|
|
51
53
|
|
|
52
54
|
useEffect(() => {
|
|
@@ -256,6 +258,7 @@ export const PlaylistTabs: React.FC<PlaylistTabsProps> = (props: PlaylistTabsPro
|
|
|
256
258
|
<Skeleton variant="rounded" width="100%" height={50} />
|
|
257
259
|
<Skeleton variant="rounded" width="100%" height={50} />
|
|
258
260
|
</Stack>
|
|
261
|
+
<Progress size={100} thickness={4} variant="indeterminate" label={false} position="absolute"/>
|
|
259
262
|
</TabPanel>
|
|
260
263
|
)}
|
|
261
264
|
</TabContext>
|
|
Binary file
|
|
@@ -159,5 +159,9 @@
|
|
|
159
159
|
"ytdlpExecutablePathHeader": "Pfad zur ausführbaren Datei yt-dpl",
|
|
160
160
|
"ytdlpExecutablePathContent": "Geben Sie einen benutzerdefinierten Pfad zur ausführbaren Datei der yt-dlp Bibliothek an, um diesen anstelle eines von der Anwendung bereitgestellten Pfads zu verwenden.",
|
|
161
161
|
"ffmpegExecutablePathHeader": "Pfad zur ausführbaren Datei ffmpeg",
|
|
162
|
-
"ffmpegExecutablePathContent": "Geben Sie einen benutzerdefinierten Pfad zur ausführbaren Datei der ffmpeg Bibliothek an, um diesen anstelle eines von der Anwendung bereitgestellten Pfads zu verwenden."
|
|
162
|
+
"ffmpegExecutablePathContent": "Geben Sie einen benutzerdefinierten Pfad zur ausführbaren Datei der ffmpeg Bibliothek an, um diesen anstelle eines von der Anwendung bereitgestellten Pfads zu verwenden.",
|
|
163
|
+
"ytdlpVersionHeader": "Informationen zur Version der yt-dlp",
|
|
164
|
+
"ytdlpVersionContent": "Zeigt die Version der yt-dlp an und ermöglicht deren Aktualisierung.",
|
|
165
|
+
"chromeExecutablePathHeader": "Pfad zur ausführbaren Datei chrome",
|
|
166
|
+
"chromeExecutablePathContent": "Pfad zur ausführbaren Chrome-Datei (leer lassen, um gebündeltes Chromium zu verwenden)."
|
|
163
167
|
}
|
|
@@ -92,9 +92,11 @@
|
|
|
92
92
|
"totalTracksCount": "Alle Spuren: {{num}}",
|
|
93
93
|
"trackOutputTemplate": "Ausgabevorlage (Audiospuren)",
|
|
94
94
|
"tracks": "Spuren",
|
|
95
|
+
"update": "Aktualisierung",
|
|
95
96
|
"videoOutputTemplate": "Ausgabevorlage (videos)",
|
|
96
97
|
"warnings": "Warnungen",
|
|
97
98
|
"year": "Jahr",
|
|
98
99
|
"youtubeUrl": "YouTube-URL",
|
|
99
|
-
"ytdlpExecutablePath": "Pfad zur ausführbaren Datei yt-dpl"
|
|
100
|
+
"ytdlpExecutablePath": "Pfad zur ausführbaren Datei yt-dpl",
|
|
101
|
+
"ytdlpVersion": "YT-DLP-Version"
|
|
100
102
|
}
|
|
@@ -159,5 +159,9 @@
|
|
|
159
159
|
"ytdlpExecutablePathHeader": "Path to yt-dlp executable",
|
|
160
160
|
"ytdlpExecutablePathContent": "Specify custom path to yt-dlp library executable to use it instead of one provided by the application.",
|
|
161
161
|
"ffmpegExecutablePathHeader": "Path to ffmpeg executable",
|
|
162
|
-
"ffmpegExecutablePathContent": "Specify custom path to ffmpeg library executable to use it instead of one provided by the application."
|
|
162
|
+
"ffmpegExecutablePathContent": "Specify custom path to ffmpeg library executable to use it instead of one provided by the application.",
|
|
163
|
+
"ytdlpVersionHeader": "Info about yt-dlp library version",
|
|
164
|
+
"ytdlpVersionContent": "Displays version of yt-dlp library and allows to update it.",
|
|
165
|
+
"chromeExecutablePathHeader": "Path to chrome executable",
|
|
166
|
+
"chromeExecutablePathContent": "Path to chrome executable (leave empty to used bundled chromium)."
|
|
163
167
|
}
|
|
@@ -92,9 +92,11 @@
|
|
|
92
92
|
"totalTracksCount": "Total tracks: {{num}}",
|
|
93
93
|
"trackOutputTemplate": "Output template (audio tracks)",
|
|
94
94
|
"tracks": "Tracks",
|
|
95
|
+
"update": "Update",
|
|
95
96
|
"videoOutputTemplate": "Output template (videos)",
|
|
96
97
|
"warnings": "Warnings",
|
|
97
98
|
"year": "Year",
|
|
98
99
|
"youtubeUrl": "YouTube URL",
|
|
99
|
-
"ytdlpExecutablePath": "Path to yt-dpl executable"
|
|
100
|
+
"ytdlpExecutablePath": "Path to yt-dpl executable",
|
|
101
|
+
"ytdlpVersion": "YT-DLP version"
|
|
100
102
|
}
|
|
@@ -159,5 +159,9 @@
|
|
|
159
159
|
"ytdlpExecutablePathHeader": "Ścieżka do pliku wykonywalnego yt-dlp",
|
|
160
160
|
"ytdlpExecutablePathContent": "Podaj ścieżkę do pliku wykonywalnego biblioteki yt-dlp aby użyć go zamiast dostarczonego wraz z aplikacją.",
|
|
161
161
|
"ffmpegExecutablePathHeader": "Ścieżka do pliku wykonywalnego ffmpeg",
|
|
162
|
-
"ffmpegExecutablePathContent": "Podaj ścieżkę do pliku wykonywalnego biblioteki ffmpeg aby użyć go zamiast dostarczonego wraz z aplikacją."
|
|
162
|
+
"ffmpegExecutablePathContent": "Podaj ścieżkę do pliku wykonywalnego biblioteki ffmpeg aby użyć go zamiast dostarczonego wraz z aplikacją.",
|
|
163
|
+
"ytdlpVersionHeader": "Informacje o bibliotece yt-dlp",
|
|
164
|
+
"ytdlpVersionContent": "Wyświetla wersję biblioteki yt-dlp i umożliwia jej aktualizację.",
|
|
165
|
+
"chromeExecutablePathHeader": "Ścieżka do pliku wykonywalnego chrome",
|
|
166
|
+
"chromeExecutablePathContent": "Ścieżka do pliku wykonywalnego chrome (pozostaw puste aby użyć chromium dostarczonego razem z aplikacją)."
|
|
163
167
|
}
|
|
@@ -92,9 +92,11 @@
|
|
|
92
92
|
"totalTracksCount": "Dostepne utwory: {{num}}",
|
|
93
93
|
"trackOutputTemplate": "Szablon wyjściowy (pliki audio)",
|
|
94
94
|
"tracks": "Ścieżki",
|
|
95
|
+
"update": "Aktualizuj",
|
|
95
96
|
"videoOutputTemplate": "Szablon wyjściowy (pliki wideo)",
|
|
96
97
|
"warnings": "Ostrzeżenia",
|
|
97
98
|
"year": "Rok",
|
|
98
99
|
"youtubeUrl": "YouTube URL",
|
|
99
|
-
"ytdlpExecutablePath": "Ścieżka do pliku wykonywalnego biblioteki yt-dlp"
|
|
100
|
+
"ytdlpExecutablePath": "Ścieżka do pliku wykonywalnego biblioteki yt-dlp",
|
|
101
|
+
"ytdlpVersion": "Wersja YT-DLP"
|
|
100
102
|
}
|
|
@@ -3,9 +3,7 @@ import React, {useEffect, useState} from "react";
|
|
|
3
3
|
import {useTranslation} from "react-i18next";
|
|
4
4
|
import {useDebounceValue} from "usehooks-ts";
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
Box, Button, Divider, FormControlLabel, Grid, Stack, Switch, TextField
|
|
8
|
-
} from "@mui/material";
|
|
6
|
+
import {Box, Button, Divider, FormControlLabel, Grid, Stack, Switch} from "@mui/material";
|
|
9
7
|
|
|
10
8
|
import {ApplicationOptions} from "../../common/Store";
|
|
11
9
|
import NumberField from "../../components/numberField/NumberField";
|
|
@@ -28,10 +26,6 @@ export const DevelopmentView: React.FC = () => {
|
|
|
28
26
|
setOptions((prev) => ({...prev, debugMode: checked}));
|
|
29
27
|
};
|
|
30
28
|
|
|
31
|
-
const handleChromeExecutablePathChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
32
|
-
setPuppeteerOptions((prev) => ({...prev, executablePath: e.target.value}));
|
|
33
|
-
};
|
|
34
|
-
|
|
35
29
|
const handleShowBrowserChange = (e: React.ChangeEvent<HTMLInputElement>, checked: boolean) => {
|
|
36
30
|
setPuppeteerOptions((prev) => ({...prev, headless: !checked}));
|
|
37
31
|
};
|
|
@@ -99,13 +93,6 @@ export const DevelopmentView: React.FC = () => {
|
|
|
99
93
|
loop
|
|
100
94
|
/>
|
|
101
95
|
</Stack>
|
|
102
|
-
<TextField
|
|
103
|
-
fullWidth
|
|
104
|
-
label={t("chromeExecutablePath")}
|
|
105
|
-
variant="outlined"
|
|
106
|
-
onChange={handleChromeExecutablePathChange}
|
|
107
|
-
value={puppeteerOptions.executablePath}
|
|
108
|
-
/>
|
|
109
96
|
</Stack>
|
|
110
97
|
</Stack>
|
|
111
98
|
<Grid className={Styles.footer}>
|
|
@@ -32,7 +32,9 @@ import {Alert, Box, Grid} from "@mui/material";
|
|
|
32
32
|
|
|
33
33
|
import {getBinPath, removeIncompleteFiles} from "../../common/FileSystem";
|
|
34
34
|
import {getAlbumInfo} from "../../common/Formatters";
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
getRealFileExtension, getUrlType, isPlaylist, mapRange, resolveMockData
|
|
37
|
+
} from "../../common/Helpers";
|
|
36
38
|
import {
|
|
37
39
|
Format, FormatScope, InputMode, MediaFormat, QueueKeys, VideoType
|
|
38
40
|
} from "../../common/Media";
|
|
@@ -168,7 +170,7 @@ export const HomeView: React.FC = () => {
|
|
|
168
170
|
if (!state.loading && hasFailures && !_isEmpty(urls)) {
|
|
169
171
|
setFailuresModalOpen(true);
|
|
170
172
|
}
|
|
171
|
-
}, [
|
|
173
|
+
}, [state.loading]);
|
|
172
174
|
|
|
173
175
|
const ytDlpWrap = useMemo<YTDlpWrap>(() => {
|
|
174
176
|
const ytdlpPath: string = global.store.get("application.ytdlpExecutablePath") || `${getBinPath()}/yt-dlp.exe`;
|
|
@@ -342,9 +344,9 @@ export const HomeView: React.FC = () => {
|
|
|
342
344
|
return resolveMockData(300);
|
|
343
345
|
} else {
|
|
344
346
|
return _map(urls, (url) => {
|
|
347
|
+
const ytdplArgs = [url, "--dump-json", "--no-check-certificate", "--geo-bypass"];
|
|
345
348
|
const controller = new AbortController();
|
|
346
349
|
abortControllers[url] = controller;
|
|
347
|
-
const ytdplArgs = [url, "--dump-json", "--no-check-certificate", "--geo-bypass"];
|
|
348
350
|
|
|
349
351
|
const promiseCreator = (ytdplArgsToUse: string[], resolve: (params: any) => any) => {
|
|
350
352
|
ytDlpWrap.execPromise(ytdplArgsToUse, undefined, controller.signal)
|
|
@@ -373,7 +375,6 @@ export const HomeView: React.FC = () => {
|
|
|
373
375
|
delete abortControllers[url];
|
|
374
376
|
});
|
|
375
377
|
};
|
|
376
|
-
|
|
377
378
|
const playlistValidationPromise = async (currentItem: number): Promise<boolean | null> => {
|
|
378
379
|
const result = await ytDlpWrap.execPromise([url, "--dump-json", "--no-check-certificate", "--geo-bypass", "--flat-playlist", "--playlist-items", `${currentItem}`], undefined);
|
|
379
380
|
const playlistCheckItemsCount = global.store.get<string, number>("application.playlistCheckItemsCount");
|
|
@@ -391,8 +392,8 @@ export const HomeView: React.FC = () => {
|
|
|
391
392
|
};
|
|
392
393
|
|
|
393
394
|
return new Promise<YoutubeInfoResult>((resolve) => {
|
|
394
|
-
if (flatPlaylist) {
|
|
395
|
-
playlistValidationPromise(1)
|
|
395
|
+
if (isPlaylist(url) && flatPlaylist) {
|
|
396
|
+
return playlistValidationPromise(1)
|
|
396
397
|
.then((result) => {
|
|
397
398
|
const ytdplArgsToUse = result ? ytdplArgs.concat("--flat-playlist") : ytdplArgs;
|
|
398
399
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {spawn} from "child_process";
|
|
1
2
|
import _filter from "lodash/filter";
|
|
2
3
|
import _first from "lodash/first";
|
|
3
4
|
import _get from "lodash/get";
|
|
@@ -11,14 +12,16 @@ import path from "path";
|
|
|
11
12
|
import React, {useEffect, useState} from "react";
|
|
12
13
|
import {useTranslation} from "react-i18next";
|
|
13
14
|
import {useDebounceValue} from "usehooks-ts";
|
|
15
|
+
import versionInfo from "win-version-info";
|
|
14
16
|
|
|
15
17
|
import NorthIcon from "@mui/icons-material/North";
|
|
16
18
|
import SouthIcon from "@mui/icons-material/South";
|
|
17
19
|
import {
|
|
18
20
|
Box, Button, FormControl, FormControlLabel, FormLabel, Grid, InputLabel, MenuItem, Paper, Radio,
|
|
19
|
-
RadioGroup, Select, SelectChangeEvent, Switch, TextField
|
|
21
|
+
RadioGroup, Select, SelectChangeEvent, Stack, Switch, TextField, Typography
|
|
20
22
|
} from "@mui/material";
|
|
21
23
|
|
|
24
|
+
import {getBinPath} from "../../common/FileSystem";
|
|
22
25
|
import {FormatScope, MultiMatchAction, SortOrder, TabsOrderKey} from "../../common/Media";
|
|
23
26
|
import StoreSchema, {ApplicationOptions} from "../../common/Store";
|
|
24
27
|
import FileField from "../../components/fileField/FileField";
|
|
@@ -31,6 +34,8 @@ export const SettingsView: React.FC = () => {
|
|
|
31
34
|
const {actions} = useAppContext();
|
|
32
35
|
const {t} = useTranslation();
|
|
33
36
|
const [validationErrors, setValidationErrors] = useState<{[key: string]: string}>({});
|
|
37
|
+
const [updatingYtDlp, setUpdatingYtDlp] = useState(false);
|
|
38
|
+
const [ytDlpVersion, setYtDlpVersion] = useState("");
|
|
34
39
|
const [applicationOptions, setApplicationOptions] = useState<ApplicationOptions>(global.store.get("application"));
|
|
35
40
|
const [debouncedApplicationOptions] = useDebounceValue(applicationOptions, 500, {leading: true});
|
|
36
41
|
const tabsOrderKeyOptions = [
|
|
@@ -40,6 +45,7 @@ export const SettingsView: React.FC = () => {
|
|
|
40
45
|
{value: "releaseYear", text: t("year")},
|
|
41
46
|
{value: "duration", text: t("duration")},
|
|
42
47
|
];
|
|
48
|
+
|
|
43
49
|
const validateTemplateString = (input: HTMLInputElement) => {
|
|
44
50
|
const allowedKeys = ["artist", "albumTitle", "trackTitle", "trackNo", "releaseYear"];
|
|
45
51
|
const regex = /{{(.*?)}}/g;
|
|
@@ -70,6 +76,15 @@ export const SettingsView: React.FC = () => {
|
|
|
70
76
|
setApplicationOptions((prev) => ({...prev, outputDirectory}));
|
|
71
77
|
};
|
|
72
78
|
|
|
79
|
+
const onChromeExecutablePathChange = (value: string[]) => {
|
|
80
|
+
setApplicationOptions((prev) => ({...prev, chromeExecutablePath: _first(value)}));
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const onChromeExecutablePathBlur = (value: string[]) => {
|
|
84
|
+
const chromeExecutablePath = _isNil(_first(value)) ? path.resolve(_get(StoreSchema.application, "properties.chromeExecutablePath.default")) : _first(value);
|
|
85
|
+
setApplicationOptions((prev) => ({...prev, chromeExecutablePath}));
|
|
86
|
+
};
|
|
87
|
+
|
|
73
88
|
const onYtdlpExecutablePathChange = (value: string[]) => {
|
|
74
89
|
setApplicationOptions((prev) => ({...prev, ytdlpExecutablePath: _first(value)}));
|
|
75
90
|
};
|
|
@@ -167,10 +182,29 @@ export const SettingsView: React.FC = () => {
|
|
|
167
182
|
const onTabsOrderOrderChange = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
168
183
|
setApplicationOptions((prev) => ({...prev, tabsOrder: [prev.tabsOrder[0], event.currentTarget.value as SortOrder]}));
|
|
169
184
|
};
|
|
185
|
+
|
|
186
|
+
const refreshYtDlpVersion = () => {
|
|
187
|
+
const info = versionInfo(global.store.get("application.ytdlpExecutablePath") || `${getBinPath()}/yt-dlp.exe`);
|
|
188
|
+
setYtDlpVersion(info.FileVersion);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const onUpdateYtDlpClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
192
|
+
const child = spawn(global.store.get("application.ytdlpExecutablePath") || `${getBinPath()}/yt-dlp.exe`, ["-U"], {shell: true});
|
|
193
|
+
|
|
194
|
+
setUpdatingYtDlp(true);
|
|
195
|
+
child.on("close", () => {
|
|
196
|
+
refreshYtDlpVersion();
|
|
197
|
+
setUpdatingYtDlp(false);
|
|
198
|
+
});
|
|
199
|
+
};
|
|
170
200
|
|
|
171
201
|
useEffect(() => {
|
|
172
202
|
global.store.set("application", debouncedApplicationOptions);
|
|
173
203
|
}, [debouncedApplicationOptions]);
|
|
204
|
+
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
refreshYtDlpVersion();
|
|
207
|
+
}, []);
|
|
174
208
|
|
|
175
209
|
return (
|
|
176
210
|
<Box className={Styles.settings}>
|
|
@@ -260,6 +294,12 @@ export const SettingsView: React.FC = () => {
|
|
|
260
294
|
<Grid size={12} data-help="mergeParts">
|
|
261
295
|
<FormControlLabel control={<Switch checked={applicationOptions.mergeParts} onChange={onMergePartsChange} />} label={t("mergeParts")} />
|
|
262
296
|
</Grid>
|
|
297
|
+
<Grid size={12} data-help="ytdlpVersion">
|
|
298
|
+
<Stack direction="row" spacing={1} className={Styles.ytdlpVersion}>
|
|
299
|
+
<Typography component="span" variant="body1">{t("ytdlpVersion")}: {ytDlpVersion}</Typography>
|
|
300
|
+
<Button size="small" variant="contained" loading={updatingYtDlp} onClick={onUpdateYtDlpClick}>{t("update")}</Button>
|
|
301
|
+
</Stack>
|
|
302
|
+
</Grid>
|
|
263
303
|
</Grid>
|
|
264
304
|
<Grid className={Styles.group} container size={6} component={Paper} variant="outlined">
|
|
265
305
|
<Grid size={12}>
|
|
@@ -349,6 +389,20 @@ export const SettingsView: React.FC = () => {
|
|
|
349
389
|
type="string"
|
|
350
390
|
/>
|
|
351
391
|
</Grid>
|
|
392
|
+
<Grid size={12}>
|
|
393
|
+
<FileField
|
|
394
|
+
data-help="chromeExecutablePath"
|
|
395
|
+
fullWidth
|
|
396
|
+
label={t("chromeExecutablePath")}
|
|
397
|
+
id="chromeExecutablePath"
|
|
398
|
+
variant="outlined"
|
|
399
|
+
onChange={onChromeExecutablePathChange}
|
|
400
|
+
onBlur={onChromeExecutablePathBlur}
|
|
401
|
+
value={applicationOptions.chromeExecutablePath}
|
|
402
|
+
mode="file"
|
|
403
|
+
fileTypes={[".exe"]}
|
|
404
|
+
/>
|
|
405
|
+
</Grid>
|
|
352
406
|
<Grid size={12}>
|
|
353
407
|
<FileField
|
|
354
408
|
data-help="ytdlpExecutablePath"
|