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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yt-grabber",
3
- "version": "1.7.0",
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);
@@ -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 getUrlType = (url: string) => {
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
- const isArtist = artistRegex.test(url);
52
- const isPlaylist = playlistRegex.test(url);
53
- const isTrack = trackRegex.test(url);
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
 
@@ -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}}",
@@ -12,4 +12,15 @@
12
12
  align-items: center;
13
13
  justify-content: center;
14
14
  }
15
+
16
+ &.absolute {
17
+ position: absolute;
18
+ display: flex;
19
+ justify-self: anchor-center;
20
+ align-self: anchor-center;
21
+ }
22
+
23
+ &.inline {
24
+ position: relative;
25
+ }
15
26
  }
@@ -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 {getRealFileExtension, getUrlType, mapRange, resolveMockData} from "../../common/Helpers";
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
- }, [trackStatus, state.loading, urls]);
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
 
@@ -22,6 +22,10 @@
22
22
  align-self: stretch;
23
23
  justify-content: center;
24
24
  }
25
+
26
+ .ytdlp-version {
27
+ align-items: center;
28
+ }
25
29
  }
26
30
 
27
31
  .footer {
@@ -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"