yt-grabber 1.8.1 → 1.8.3

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,7 +1,29 @@
1
1
  {
2
2
  "name": "yt-grabber",
3
- "version": "1.8.1",
4
- "description": "Youtube Grabber",
3
+ "version": "1.8.3",
4
+ "description": "Youtube Grabber - robust desktop application designed to retrieve multimedia from YouTube and YouTube Music services",
5
+ "keywords": [
6
+ "youtube",
7
+ "youtube music",
8
+ "grabber",
9
+ "downloader",
10
+ "music",
11
+ "albums",
12
+ "artists",
13
+ "album releases",
14
+ "songs",
15
+ "discography",
16
+ "yt-dlp",
17
+ "youtube playlist",
18
+ "youtube channel",
19
+ "youtube video",
20
+ "youtube audio",
21
+ "youtube mp3",
22
+ "youtube mp4",
23
+ "youtube mpeg",
24
+ "youtube mkv",
25
+ "youtube avi"
26
+ ],
5
27
  "main": "./dist/index.js",
6
28
  "repository": {
7
29
  "url": "https://github.com/karenpommeroy/yt-grabber.git"
@@ -37,3 +37,11 @@ export const YtMusicArtistRelativeNameSelector = ".//div[contains(@class, 'title
37
37
  export const YtMusicArtistRelativeLinkSelector = ".//a";
38
38
 
39
39
  export const getYtMusicSearchResultsArtistsSelector = (artist: string) => `//ytmusic-app-layout//div[@id='content']//ytmusic-search-page//div[@id='contents']//ytmusic-shelf-renderer//div[@id='contents']//ytmusic-responsive-list-item-renderer//div[contains(@class, 'title-column')]/yt-formatted-string//text()[translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÜÉÈÊÀÁÂÒÓÔÙÚÛÇÅÏÕÑŒĄĆĘŁŃÓŚŹŻ', 'abcdefghijklmnopqrstuvwxyzäöüéèêàáâòóôùúûçåïõñœąćęłńóśźż')='${_toLower(artist)}' or translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÜÉÈÊÀÁÂÒÓÔÙÚÛÇÅÏÕÑŒĄĆĘŁŃÓŚŹŻ', 'abcdefghijklmnopqrstuvwxyzäöüéèêàáâòóôùúûçåïõñœąćęłńóśźż')='the ${_toLower(artist)}']//ancestor::ytmusic-responsive-list-item-renderer`;
40
+
41
+ export const getYtMusicAlbumsDirectLinkSelectorFilteredByDate = (fromYear = 0, untilYear = 9999) => `//ytmusic-app-layout//div[@id='content']/ytmusic-browse-response//ytmusic-section-list-renderer//div[@id='content-group']//div[contains(@class, 'header-renderer')]/yt-formatted-string[contains(text(), 'Album')]//ancestor::div[contains(@class, 'ytmusic-shelf')]//div[@id='items-wrapper']/ul/*[contains(@class, 'ytmusic-carousel')]//div[contains(@class, 'details')]/span/yt-formatted-string[number(text()) >= ${fromYear} and number(text()) <= ${untilYear}]//ancestor::div[contains(@class, 'details')]//a[contains(@class, 'yt-simple-endpoint') and contains(@class, 'yt-formatted-string')]`;
42
+
43
+ export const getYtMusicSinglesDirectLinkSelectorFilteredByDate = (fromYear = 0, untilYear = 9999) => `//ytmusic-app-layout//div[@id='content']/ytmusic-browse-response//ytmusic-section-list-renderer//div[@id='content-group']//div[contains(@class, 'header-renderer')]/yt-formatted-string[contains(text(), 'Single')]//ancestor::div[contains(@class, 'ytmusic-shelf')]//div[@id='items-wrapper']/ul/*[contains(@class, 'ytmusic-carousel')]//div[contains(@class, 'details')]/span/yt-formatted-string[number(text()) >= ${fromYear} and number(text()) <= ${untilYear}]//ancestor::div[contains(@class, 'details')]//a[contains(@class, 'yt-simple-endpoint') and contains(@class, 'yt-formatted-string')]`;
44
+
45
+ export const getYtMusicAlbumLinkSelectorFilteredByDate = (fromYear = 0, untilYear = 9999) => `//ytmusic-app-layout//div[@id='content']/ytmusic-browse-response//ytmusic-section-list-renderer/div[@id='contents']/ytmusic-grid-renderer/div[@id='items']/*[contains(@class, 'ytmusic-grid-renderer')]//div[contains(@class, 'details')]/span/yt-formatted-string/span[number(text()) >= ${fromYear} and number(text()) <= ${untilYear}]//ancestor::div[contains(@class, 'details')]//a[contains(@class, 'yt-simple-endpoint') and contains(@class, 'yt-formatted-string')]`;
46
+
47
+ export const getYtMusicSingleLinkSelectorFilteredByDate = (fromYear = 0, untilYear = 9999) => `//ytmusic-app-layout//div[@id='content']/ytmusic-browse-response//ytmusic-section-list-renderer/div[@id='contents']/ytmusic-grid-renderer/div[@id='items']/*[contains(@class, 'ytmusic-grid-renderer')]//div[contains(@class, 'details')]/span/yt-formatted-string/span[number(text()) >= ${fromYear} and number(text()) <= ${untilYear}]//ancestor::div[contains(@class, 'details')]//a[contains(@class, 'yt-simple-endpoint') and contains(@class, 'yt-formatted-string')]`;
@@ -18,7 +18,9 @@ import {MessageHandlerParams} from "../messaging/MessageChannel";
18
18
  import {clearInput, navigateToPage, setCookies} from "./Helpers";
19
19
  import {
20
20
  AlbumFilterSelector, AlbumLinkSelector, AlbumsDirectLinkSelector, AlbumsHrefSelector,
21
- getYtMusicSearchResultsArtistsSelector, SingleFilterSelector, SingleLinkSelector,
21
+ getYtMusicAlbumLinkSelectorFilteredByDate, getYtMusicAlbumsDirectLinkSelectorFilteredByDate,
22
+ getYtMusicSearchResultsArtistsSelector, getYtMusicSingleLinkSelectorFilteredByDate,
23
+ getYtMusicSinglesDirectLinkSelectorFilteredByDate, SingleFilterSelector, SingleLinkSelector,
22
24
  SinglesDirectLinkSelector, SinglesHrefSelector, YtMusicArtistBestResultLinkSelector,
23
25
  YtMusicArtistRelativeLinkSelector, YtMusicArtistRelativeNameSelector,
24
26
  YtMusicArtistRelativeThumbnailSelector, YtMusicArtistsChipSelector, YtMusicSearchInputSelector,
@@ -90,9 +92,11 @@ const run = async (
90
92
  await navigateToPage(artistChannelUrl, page);
91
93
  await page.waitForNetworkIdle();
92
94
 
93
- const albums = await getAlbums(params);
94
- results.push(...albums);
95
-
95
+ if (params.options?.downloadAlbums) {
96
+ const albums = await getAlbums(params);
97
+ results.push(...albums);
98
+ }
99
+
96
100
  if (params.options?.downloadSinglesAndEps) {
97
101
  await navigateToPage(artistChannelUrl, page);
98
102
  await page.waitForNetworkIdle();
@@ -168,6 +172,7 @@ const getArtistUrl = async (params: GetYoutubeParams, artist: string, onPause?:
168
172
 
169
173
  const getAlbums = async (params: GetYoutubeParams): Promise<string[]> => {
170
174
  const results: string[] = [];
175
+ const {fromYear, untilYear} = params.options;
171
176
 
172
177
  try {
173
178
  const element = await page.waitForSelector(`::-p-xpath(${AlbumsHrefSelector})`, {timeout: 1000});
@@ -184,16 +189,19 @@ const getAlbums = async (params: GetYoutubeParams): Promise<string[]> => {
184
189
  } catch (e) {
185
190
  console.log("Albums already filtered");
186
191
  } finally {
187
- const items = await page.$$eval(`xpath/${AlbumLinkSelector}`, (elements) => elements.map((el) => el.getAttribute("href")));
192
+ const selector = fromYear || untilYear ? getYtMusicAlbumLinkSelectorFilteredByDate(fromYear, untilYear) : AlbumLinkSelector;
193
+ const items = await page.$$eval(`xpath/${selector}`, (elements) => elements.map((el) => el.getAttribute("href")));
188
194
 
189
195
  for (const item of items) {
190
196
  results.push(`${params.url}/${item}`);
191
197
  }
192
198
 
199
+ // eslint-disable-next-line no-unsafe-finally
193
200
  return results;
194
201
  }
195
202
  } catch (error) {
196
- const albums = await page.$$eval(`xpath/${AlbumsDirectLinkSelector}`, (elements) => elements.map((el) => el.getAttribute("href")));
203
+ const selector = fromYear || untilYear ? getYtMusicAlbumsDirectLinkSelectorFilteredByDate(fromYear, untilYear) : AlbumsDirectLinkSelector;
204
+ const albums = await page.$$eval(`xpath/${selector}`, (elements) => elements.map((el) => el.getAttribute("href")));
197
205
 
198
206
  for (const item of albums) {
199
207
  results.push(`${params.url}/${item}`);
@@ -205,6 +213,7 @@ const getAlbums = async (params: GetYoutubeParams): Promise<string[]> => {
205
213
 
206
214
  const getSingles = async (params: GetYoutubeParams): Promise<string[]> => {
207
215
  const results: string[] = [];
216
+ const {fromYear, untilYear} = params.options;
208
217
 
209
218
  try {
210
219
  const element = await page.waitForSelector(`::-p-xpath(${SinglesHrefSelector})`, {timeout: 1000});
@@ -221,16 +230,19 @@ const getSingles = async (params: GetYoutubeParams): Promise<string[]> => {
221
230
  } catch (e) {
222
231
  console.log("Singles already filtered");
223
232
  } finally {
224
- const items = await page.$$eval(`xpath/${SingleLinkSelector}`, (elements) => elements.map((el) => el.getAttribute("href")));
233
+ const selector = fromYear || untilYear ? getYtMusicSingleLinkSelectorFilteredByDate(fromYear, untilYear) : SingleLinkSelector;
234
+ const items = await page.$$eval(`xpath/${selector}`, (elements) => elements.map((el) => el.getAttribute("href")));
225
235
 
226
236
  for (const item of items) {
227
237
  results.push(`${params.url}/${item}`);
228
238
  }
229
239
 
240
+ // eslint-disable-next-line no-unsafe-finally
230
241
  return results;
231
242
  }
232
243
  } catch (error) {
233
- const singles = await page.$$eval(`xpath/${SinglesDirectLinkSelector}`, (elements) => elements.map((el) => el.getAttribute("href")));
244
+ const selector = fromYear || untilYear ? getYtMusicSinglesDirectLinkSelectorFilteredByDate(fromYear, untilYear) : SinglesDirectLinkSelector;
245
+ const singles = await page.$$eval(`xpath/${selector}`, (elements) => elements.map((el) => el.getAttribute("href")));
234
246
 
235
247
  for (const item of singles) {
236
248
  results.push(`${params.url}/${item}`);
@@ -20,6 +20,7 @@ export type OpenUrlInBrowserParams = {
20
20
 
21
21
  export type GetYoutubeParams = {
22
22
  values?: string[];
23
+ fromYear?: string;
23
24
  lang: string;
24
25
  url: string;
25
26
  options?: Record<string, any>;
@@ -25,6 +25,8 @@ export type ApplicationOptions = {
25
25
  alwaysOverwrite?: boolean;
26
26
  mergeParts?: boolean;
27
27
  downloadSinglesAndEps?: boolean;
28
+ downloadAlbums?: boolean;
29
+ showAdvancedSearchOptions?: boolean;
28
30
  inputMode?: InputMode;
29
31
  tabsOrder?: [TabsOrderKey, SortOrder];
30
32
  };
@@ -134,10 +136,18 @@ export const StoreSchema: Schema<IStore> = {
134
136
  type: "boolean",
135
137
  default: true
136
138
  },
139
+ downloadAlbums: {
140
+ type: "boolean",
141
+ default: true
142
+ },
137
143
  downloadSinglesAndEps: {
138
144
  type: "boolean",
139
145
  default: false
140
146
  },
147
+ showAdvancedSearchOptions: {
148
+ type: "boolean",
149
+ default: false
150
+ },
141
151
  inputMode: {
142
152
  type: "string",
143
153
  default: InputMode.Auto,
@@ -27,6 +27,7 @@ export const getYtdplRequestParams = (track: TrackInfo, album: AlbumInfo, trackC
27
27
  "--progress",
28
28
  ...getCutArgs(track, trackCuts),
29
29
  appOptions.alwaysOverwrite ? "--force-overwrite" : "",
30
+ "--extractor-args", "youtube:player-client=default,-tv_simply",
30
31
  "--postprocessor-args", getPostProcessorArgs(track, album),
31
32
  "--output", getOutput(track, album, format, trackCuts)
32
33
  ];
@@ -151,7 +151,7 @@ export const FormatSelector: React.FC<FormatSelectorProps> = (props) => {
151
151
 
152
152
  return (
153
153
  <Grid className={Styles.formatSelector} container spacing={2} padding={2}>
154
- <Grid size={4}>
154
+ <Grid size="grow">
155
155
  <FormControl fullWidth disabled={disabled} data-help="mediaType">
156
156
  <InputLabel id="media-type-label">{t("mediaType")}</InputLabel>
157
157
  <Select<MediaFormat>
@@ -167,7 +167,7 @@ export const FormatSelector: React.FC<FormatSelectorProps> = (props) => {
167
167
  </Select>
168
168
  </FormControl>
169
169
  </Grid>
170
- <Grid size={4}>
170
+ <Grid size="grow">
171
171
  <FormControl fullWidth disabled={disabled} data-help="format">
172
172
  <InputLabel id="format-label">{t("format")}</InputLabel>
173
173
  <Select<string>
@@ -181,7 +181,7 @@ export const FormatSelector: React.FC<FormatSelectorProps> = (props) => {
181
181
  </FormControl>
182
182
  </Grid>
183
183
  {selectedMediaType === MediaFormat.Video &&
184
- <Grid size={4}>
184
+ <Grid size="grow">
185
185
  <FormControl fullWidth disabled={disabled} data-help="resolution">
186
186
  <InputLabel id="resolution-label">{t("resolution")}</InputLabel>
187
187
  <Select<string>
@@ -196,7 +196,7 @@ export const FormatSelector: React.FC<FormatSelectorProps> = (props) => {
196
196
  </Grid>
197
197
  }
198
198
  {selectedMediaType === MediaFormat.Audio &&
199
- <Grid size={4}>
199
+ <Grid size="grow">
200
200
  <NumberField
201
201
  data-help="audioQuality"
202
202
  disabled={disabled}
@@ -9,6 +9,41 @@
9
9
  display: flex;
10
10
  }
11
11
  }
12
+ }
13
+
14
+ .accordion {
15
+ background: transparent;
16
+ background-image: none;
17
+
18
+ .accordion-summary {
19
+ border-radius: 8px;
20
+ background-color: var(--theme-palette-background-paper);
21
+ background-color: var(--theme-palette-primary-dark);
22
+ background-image: var(--Paper-overlay);
23
+ border: 1px solid var(--theme-palette-divider);
24
+ }
25
+
26
+ &:global(.Mui-expanded) {
27
+ .accordion-summary {
28
+ border-bottom-left-radius: 0;
29
+ border-bottom-right-radius: 0;
30
+ }
31
+ }
32
+
33
+ .accordion-details {
34
+ border-radius: 8px;
35
+ border-top-left-radius: 0;
36
+ border-top-right-radius: 0;
37
+ border: 1px solid var(--theme-palette-divider);
38
+ border-top: none;
39
+ }
40
+ }
41
+
42
+ .text-input-group {
43
+ gap: var(--Grid-rowSpacing) var(--Grid-columnSpacing);
12
44
 
45
+ .control-group {
46
+ gap: var(--Grid-rowSpacing) var(--Grid-columnSpacing);
47
+ }
13
48
  }
14
49
  }
@@ -8,17 +8,20 @@ import _replace from "lodash/replace";
8
8
  import _truncate from "lodash/truncate";
9
9
  import _uniq from "lodash/uniq";
10
10
  import _without from "lodash/without";
11
- import React, {useCallback, useEffect, useMemo, useRef, useState} from "react";
11
+ import React, {ChangeEvent, useCallback, useEffect, useMemo, useRef, useState} from "react";
12
12
  import {useTranslation} from "react-i18next";
13
13
  import {useDebounceValue} from "usehooks-ts";
14
14
 
15
15
  import ClearIcon from "@mui/icons-material/Clear";
16
16
  import DownloadIcon from "@mui/icons-material/Download";
17
+ import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
17
18
  import FolderIcon from "@mui/icons-material/Folder";
18
19
  import ReplayIcon from "@mui/icons-material/Replay";
19
20
  import SearchIcon from "@mui/icons-material/Search";
20
21
  import {
21
- Autocomplete, AutocompleteRenderInputParams, Button, Chip, Grid, Stack, TextField, Tooltip
22
+ Accordion, AccordionDetails, AccordionSummary, Autocomplete, AutocompleteRenderInputParams,
23
+ Button, Checkbox, Chip, FormControl, FormControlLabel, FormGroup, FormLabel, Grid, Stack,
24
+ TextField, Tooltip, Typography
22
25
  } from "@mui/material";
23
26
 
24
27
  import {getUrlType} from "../../../common/Helpers";
@@ -40,14 +43,16 @@ export type InputPanelProps = {
40
43
 
41
44
  export const InputPanel: React.FC<InputPanelProps> = (props: InputPanelProps) => {
42
45
  const {loading, onDownload, onCancel, onDownloadFailed, onChange, onLoadInfo} = props;
43
- const [options] = useState<ApplicationOptions>(global.store.get("application"));
44
- const [debouncedOptions] = useDebounceValue(options, 500, {leading: true});
46
+ const [applicationOptions, setApplicationOptions] = useState<ApplicationOptions>(global.store.get("application"));
47
+ const [debouncedApplicationOptions] = useDebounceValue(applicationOptions, 500, {leading: true, trailing: true});
45
48
  const {trackStatus, urls, setUrls} = useDataState();
46
49
  const {t} = useTranslation();
47
50
  const fileInputRef = useRef<HTMLInputElement>(null);
48
51
  const valueCount = urls.length;
52
+ const [fromYear, setFromYear] = useState<string>();
53
+ const [untilYear, setUntilYear] = useState<string>();
49
54
  const [inputMode, setInputMode] = useState<InputMode>(global.store.get("application.inputMode"));
50
-
55
+
51
56
  const truncateRegex = /^(?:https?:\/\/)?(?:www\.)?(?:m\.)?(?:music\.)?(?:youtube\.com\/(?:watch\?v=|embed\/|browse\/|channel\/|shorts\/|live\/|playlist\?list=)|youtu\.be\/)/;
52
57
  const validateRegex = /^(?:https?:\/\/)?(?:www\.)?(?:m\.)?(?:music\.)?(?:youtube\.com\/(?:watch\?v=|embed\/|browse\/|channel\/|shorts\/|live\/|playlist\?list=)|youtu\.be\/)([\w-]{11})/;
53
58
 
@@ -61,10 +66,6 @@ export const InputPanel: React.FC<InputPanelProps> = (props: InputPanelProps) =>
61
66
  return options.debugMode ? true : validateRegex.test(value);
62
67
  };
63
68
 
64
- useEffect(() => {
65
- global.store.set("application", debouncedOptions);
66
- }, [debouncedOptions]);
67
-
68
69
  useEffect(() => {
69
70
  const unsubscribeInputMode = global.store.onDidChange<any>("application.inputMode", (newInputMode: InputMode) => {
70
71
  setInputMode(newInputMode);
@@ -73,24 +74,24 @@ export const InputPanel: React.FC<InputPanelProps> = (props: InputPanelProps) =>
73
74
  return () => {
74
75
  unsubscribeInputMode();
75
76
  };
76
- }, []);
77
+ }, []);
77
78
 
78
79
  const handleDelete = useCallback((valueToDelete: string) => {
79
80
  const newUrls = _without(urls, valueToDelete);
80
81
 
81
82
  setUrls((prev) => _without(prev, valueToDelete));
82
-
83
+
83
84
  if (_isFunction(onChange)) {
84
85
  onChange(newUrls);
85
86
  }
86
87
  }, [urls]);
87
-
88
+
88
89
  const handleOpenFromFile = () => {
89
90
  fileInputRef.current?.click();
90
91
  };
91
92
 
92
93
  const handleLoadInfo = () => {
93
- onLoadInfo(urls);
94
+ onLoadInfo(urls, fromYear, untilYear);
94
95
  };
95
96
 
96
97
  const containsInvalidValues = useMemo(() => {
@@ -102,10 +103,10 @@ export const InputPanel: React.FC<InputPanelProps> = (props: InputPanelProps) =>
102
103
  }, [trackStatus]);
103
104
 
104
105
  const onMultiValueChange = (value: React.ChangeEvent<HTMLInputElement>, newValue: []) => {
105
- const newUrls = _uniq(_filter(newValue, isValid));
106
+ const newUrls = _uniq(_filter(newValue, isValid));
106
107
 
107
108
  setUrls(newUrls);
108
-
109
+
109
110
  if (_isFunction(onChange)) {
110
111
  onChange(newUrls);
111
112
  }
@@ -121,9 +122,9 @@ export const InputPanel: React.FC<InputPanelProps> = (props: InputPanelProps) =>
121
122
 
122
123
  const onSelectFile = (event: React.ChangeEvent<HTMLInputElement>) => {
123
124
  const file = event.target.files?.[0];
124
-
125
+
125
126
  if (!file) return;
126
-
127
+
127
128
  const reader = new FileReader();
128
129
 
129
130
  reader.onload = (e) => {
@@ -138,18 +139,42 @@ export const InputPanel: React.FC<InputPanelProps> = (props: InputPanelProps) =>
138
139
  event.target.value = "";
139
140
  };
140
141
 
142
+ const onShowAdvancedSearchOptionsChange = (e: React.SyntheticEvent<HTMLDivElement>, isExpanded: boolean) => {
143
+ setApplicationOptions((prev) => ({...prev, showAdvancedSearchOptions: isExpanded}));
144
+ };
145
+
146
+ const onDownloadSinglesAndEpsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
147
+ setApplicationOptions((prev) => ({...prev, downloadSinglesAndEps: e.target.checked}));
148
+ };
149
+
150
+ const onDownloadAlbumsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
151
+ setApplicationOptions((prev) => ({...prev, downloadAlbums: e.target.checked}));
152
+ };
153
+
154
+ const onFromYearChanged = (event: ChangeEvent<HTMLInputElement>) => {
155
+ const value = parseInt(event.target.value);
156
+
157
+ setFromYear(isNaN(value) ? undefined : event.target.value);
158
+ };
159
+
160
+ const onUntilYearChanged = (event: ChangeEvent<HTMLInputElement>) => {
161
+ const value = parseInt(event.target.value);
162
+
163
+ setUntilYear(isNaN(value) ? undefined : event.target.value);
164
+ };
165
+
141
166
  const getInputLabel = () => {
142
167
  switch (inputMode) {
143
- case InputMode.Auto:
144
- return t("youtubeUrl");
145
- case InputMode.Artists:
146
- return t("artistOrArtists");
147
- case InputMode.Albums:
148
- return t("albumOrAlbums");
149
- case InputMode.Songs:
150
- return t("songOrSongs");
151
- default:
152
- return t("youtubeUrl");
168
+ case InputMode.Auto:
169
+ return t("youtubeUrl");
170
+ case InputMode.Artists:
171
+ return t("artistOrArtists");
172
+ case InputMode.Albums:
173
+ return t("albumOrAlbums");
174
+ case InputMode.Songs:
175
+ return t("songOrSongs");
176
+ default:
177
+ return t("youtubeUrl");
153
178
  }
154
179
  };
155
180
 
@@ -183,7 +208,11 @@ export const InputPanel: React.FC<InputPanelProps> = (props: InputPanelProps) =>
183
208
  </Tooltip>
184
209
  );
185
210
  }, [inputMode]);
186
-
211
+
212
+ useEffect(() => {
213
+ global.store.set("application", debouncedApplicationOptions);
214
+ }, [debouncedApplicationOptions]);
215
+
187
216
  return (
188
217
  <Grid className={Styles.inputPanel} container spacing={2} padding={2} paddingBottom={1}>
189
218
  <Grid size="grow">
@@ -226,7 +255,7 @@ export const InputPanel: React.FC<InputPanelProps> = (props: InputPanelProps) =>
226
255
  <Tooltip title={t("loadFromFile")} arrow enterDelay={2000} leaveDelay={100} enterNextDelay={500} placement="bottom">
227
256
  <div>
228
257
  <Button data-help="loadFromFile" disabled={loading} variant="contained" disableElevation color="secondary" onClick={() => handleOpenFromFile()}>
229
- <FolderIcon/>
258
+ <FolderIcon />
230
259
  </Button>
231
260
  </div>
232
261
  </Tooltip>
@@ -264,6 +293,58 @@ export const InputPanel: React.FC<InputPanelProps> = (props: InputPanelProps) =>
264
293
  }
265
294
  </Stack>
266
295
  </Grid>
296
+ {global.store.get("application.inputMode") === InputMode.Artists &&
297
+ <Grid size={12}>
298
+ <Accordion
299
+ elevation={0}
300
+ className={Styles.accordion}
301
+ data-help="showAdvancedSearchOptions"
302
+ disableGutters
303
+ expanded={applicationOptions.showAdvancedSearchOptions}
304
+ onChange={onShowAdvancedSearchOptionsChange}
305
+ >
306
+ <AccordionSummary expandIcon={<ExpandMoreIcon />} className={Styles.accordionSummary}>
307
+ <Typography variant="body1">{t("showAdvancedSearchOptions")}</Typography>
308
+ </AccordionSummary>
309
+ <AccordionDetails className={Styles.accordionDetails}>
310
+ <Stack direction="column" spacing={1} paddingX={0} paddingY={2} paddingBottom={0}>
311
+ <FormControl className={Styles.textInputGroup} data-help="downloadReleaseDate">
312
+ <FormLabel component="legend">{t("releaseDate")}</FormLabel>
313
+ <FormGroup row className={Styles.controlGroup}>
314
+ <TextField data-help="downloadReleaseDateFrom" label={t("fromYear")} variant="outlined" value={fromYear} onChange={onFromYearChanged} />
315
+ <TextField data-help="downloadReleaseDateUntil" label={t("untilYear")} variant="outlined" value={untilYear} onChange={onUntilYearChanged}/>
316
+ </FormGroup>
317
+ </FormControl>
318
+ <FormControl data-help="downloadReleaseType">
319
+ <FormLabel component="legend">{t("download")}</FormLabel>
320
+ <FormGroup row>
321
+ <FormControlLabel
322
+ data-help="downloadAlbums"
323
+ label={t("downloadAlbums")}
324
+ control={
325
+ <Checkbox
326
+ checked={applicationOptions.downloadAlbums}
327
+ onChange={onDownloadAlbumsChange}
328
+ />
329
+ }
330
+ />
331
+ <FormControlLabel
332
+ data-help="downloadSinglesAndEps"
333
+ label={t("downloadSinglesAndEps")}
334
+ control={
335
+ <Checkbox
336
+ checked={applicationOptions.downloadSinglesAndEps}
337
+ onChange={onDownloadSinglesAndEpsChange}
338
+ />
339
+ }
340
+ />
341
+ </FormGroup>
342
+ </FormControl>
343
+ </Stack>
344
+ </AccordionDetails>
345
+ </Accordion>
346
+ </Grid>
347
+ }
267
348
  </Grid>
268
349
  );
269
350
  };
@@ -50,6 +50,7 @@ progress-bg-color = var(--theme-palette-primary-light)
50
50
  white-space: nowrap;
51
51
  overflow: hidden;
52
52
  text-overflow: ellipsis;
53
+ font-size: 0.8rem;
53
54
  }
54
55
 
55
56
  .tab-icon {
@@ -15,7 +15,6 @@ 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";
19
18
 
20
19
  import CloseIcon from "@mui/icons-material/Close";
21
20
  import TabContext from "@mui/lab/TabContext";
@@ -48,7 +47,6 @@ export const PlaylistTabs: React.FC<PlaylistTabsProps> = (props: PlaylistTabsPro
48
47
  const {queue, onDownloadTrack, onDownloadPlaylist, onCancelPlaylist, onCancelTrack} = props;
49
48
  const {trackStatus, playlists, activeTab, setActiveTab, setTrackStatus, setPlaylists, setTracks} = useDataState();
50
49
  const {state} = useAppContext();
51
- const {t} = useTranslation();
52
50
  const tabWidth = window.innerWidth / (playlists.length + playlists.length) - 30;
53
51
 
54
52
  useEffect(() => {
@@ -196,6 +194,7 @@ export const PlaylistTabs: React.FC<PlaylistTabsProps> = (props: PlaylistTabsPro
196
194
  return <Tab
197
195
  key={item.album.id}
198
196
  className={Styles.tab}
197
+
199
198
  icon={
200
199
  <Badge
201
200
  className={Styles.tabRemoveButton}
@@ -146,7 +146,7 @@
146
146
  "tabsOrderContent": "Ascending or descending order.",
147
147
  "editTrackHeader": "Trackdaten bearbeiten",
148
148
  "editTrackContent": "Ermöglicht das Bearbeiten von Trackdaten wie dem Titel.",
149
- "downloadSinglesAndEpsHeader": "Download Singles und EPs",
149
+ "downloadSinglesAndEpsHeader": "Singles und EPs",
150
150
  "downloadSinglesAndEpsContent": "Schließen Sie beim Herunterladen von Künstleralben auch Singles und EPs ein.",
151
151
  "tabsOrderFieldHeader": "Tabs Reihenfolge Attribut",
152
152
  "tabsOrderFieldContent": "Gibt das Attribut an, das zum Sortieren der Registerkarten verwendet wird.",
@@ -163,5 +163,17 @@
163
163
  "ytdlpVersionHeader": "Informationen zur Version der yt-dlp",
164
164
  "ytdlpVersionContent": "Zeigt die Version der yt-dlp an und ermöglicht deren Aktualisierung.",
165
165
  "chromeExecutablePathHeader": "Pfad zur ausführbaren Datei chrome",
166
- "chromeExecutablePathContent": "Pfad zur ausführbaren Chrome-Datei (leer lassen, um gebündeltes Chromium zu verwenden)."
166
+ "chromeExecutablePathContent": "Pfad zur ausführbaren Chrome-Datei (leer lassen, um gebündeltes Chromium zu verwenden).",
167
+ "showAdvancedSearchOptionsHeader": "Erweiterte Suchoptionen",
168
+ "showAdvancedSearchOptionsContent": "Zeigt das Feld mit den erweiterten Suchoptionen an.",
169
+ "downloadReleaseTypeHeader": "Veröffentlichungstyp herunterladen",
170
+ "downloadReleaseTypeContent": "Geben Sie die Typen der Elemente an, die heruntergeladen werden sollen.",
171
+ "downloadReleaseDateHeader": "Veröffentlichungsdatum herunterladen",
172
+ "downloadReleaseDateContent": "Geben Sie den Veröffentlichungszeitraum der herunterzuladenden Elemente an.",
173
+ "downloadReleaseDateFromHeader": "Ab Jahr",
174
+ "downloadReleaseDateFromContent": "Elemente mit einem Veröffentlichungsdatum vor diesem Jahr werden ausgelassen.",
175
+ "downloadReleaseDateUntilHeader": "Bis Jahr",
176
+ "downloadReleaseDateUntilContent": "Elemente mit einem Veröffentlichungsdatum nach diesem Jahr werden ausgelassen.",
177
+ "downloadAlbumsHeader": "Alben",
178
+ "downloadAlbumsContent": "Schließen Sie beim Herunterladen der Alben des Künstlers vollständige Albumveröffentlichungen ein."
167
179
  }
@@ -22,13 +22,14 @@
22
22
  "detailsModalTitle": "Mediendetails bearbeiten",
23
23
  "done": "Fertig",
24
24
  "download": "Herunterladen",
25
+ "downloadAlbums": "Alben",
25
26
  "downloadAll": "Alles herunterladen",
26
27
  "downloadedPlaylistsCount": "Heruntergeladene Wiedergabelisten: {{num}}",
27
28
  "downloadedTracksCount": "Heruntergeladene Spuren: {{num}}",
28
29
  "downloadFailed": "Falsch Herunterladen",
29
30
  "downloading": "Herunterladen läuft",
30
31
  "downloadPlaylist": "Playlist herunterladen",
31
- "downloadSinglesAndEps": "Download Singles und EPs",
32
+ "downloadSinglesAndEps": "Singles und EPs",
32
33
  "duration": "Dauer",
33
34
  "edit": "Bearbeiten",
34
35
  "errors": "Fehler",
@@ -47,6 +48,7 @@
47
48
  "formatScopeTab": "Für jeden Tab unterschiedlich",
48
49
  "foundDeletedOrPrivateMedia": "Einige gelöschte oder private Medien wurden gefunden. Sie wurden übersprungen und werden in den Ergebnissen nicht angezeigt.",
49
50
  "from": "von",
51
+ "fromYear": "Von (Jahr)",
50
52
  "invalidTemplateKeys": "Ungültige Vorlagenschlüssel: {{invalidKeys}}",
51
53
  "langName": "Deutsch",
52
54
  "loadFromFile": "Aus Datei laden",
@@ -74,11 +76,13 @@
74
76
  "playlistOutputTemplate": "Ausgabevorlage (Wiedergabeliste)",
75
77
  "playlists": "Wiedergabelisten",
76
78
  "reading": "Lesen",
79
+ "releaseDate": "Veröffentlichungsdatum",
77
80
  "releaseYear": "Erscheinungsjahr",
78
81
  "resolution": "Qualität",
79
82
  "retry": "Wiederholen",
80
83
  "selectArtist": "Künstler auswählen",
81
84
  "selectInputMode": "Eingabemodus",
85
+ "showAdvancedSearchOptions": "Erweiterte Suchoptionen",
82
86
  "showBrowser": "Webbrowser anzeigen",
83
87
  "songOrSongs": "Liedtitel",
84
88
  "songs": "Lieder",
@@ -92,6 +96,7 @@
92
96
  "totalTracksCount": "Alle Spuren: {{num}}",
93
97
  "trackOutputTemplate": "Ausgabevorlage (Audiospuren)",
94
98
  "tracks": "Spuren",
99
+ "untilYear": "Bis (Jahr)",
95
100
  "update": "Aktualisierung",
96
101
  "videoOutputTemplate": "Ausgabevorlage (videos)",
97
102
  "warnings": "Warnungen",
@@ -146,7 +146,7 @@
146
146
  "tabsOrderContent": "Ascending or descending order.",
147
147
  "editTrackHeader": "Edit track info",
148
148
  "editTrackContent": "Allows to edit track details such as title.",
149
- "downloadSinglesAndEpsHeader": "Download singles and eps",
149
+ "downloadSinglesAndEpsHeader": "Singles and EPs",
150
150
  "downloadSinglesAndEpsContent": "Include singles and EPs when downloading artist's albums.",
151
151
  "tabsOrderFieldHeader": "Tabs order attribute",
152
152
  "tabsOrderFieldContent": "Specifies attribute used to order tabs.",
@@ -163,5 +163,17 @@
163
163
  "ytdlpVersionHeader": "Info about yt-dlp library version",
164
164
  "ytdlpVersionContent": "Displays version of yt-dlp library and allows to update it.",
165
165
  "chromeExecutablePathHeader": "Path to chrome executable",
166
- "chromeExecutablePathContent": "Path to chrome executable (leave empty to used bundled chromium)."
166
+ "chromeExecutablePathContent": "Path to chrome executable (leave empty to used bundled chromium).",
167
+ "showAdvancedSearchOptionsHeader": "Advanced search options",
168
+ "showAdvancedSearchOptionsContent": "Displays advanced search options panel.",
169
+ "downloadReleaseTypeHeader": "Download release type",
170
+ "downloadReleaseTypeContent": "Specify types of items to be downloaded.",
171
+ "downloadReleaseDateHeader": "Download release date",
172
+ "downloadReleaseDateContent": "Specify release date range of items to be downloaded.",
173
+ "downloadReleaseDateFromHeader": "From year",
174
+ "downloadReleaseDateFromContent": "Items with release date prior to this year will be omitted",
175
+ "downloadReleaseDateUntilHeader": "Until year",
176
+ "downloadReleaseDateUntilContent": "Items with release date after this year will be omitted.",
177
+ "downloadAlbumsHeader": "Albums",
178
+ "downloadAlbumsContent": "Include full album releases when downloading artist's albums."
167
179
  }
@@ -22,13 +22,14 @@
22
22
  "detailsModalTitle": "Edit media details",
23
23
  "done": "Done",
24
24
  "download": "Download",
25
+ "downloadAlbums": "Albums",
25
26
  "downloadAll": "Download All",
26
27
  "downloadedPlaylistsCount": "Downloaded playlists: {{num}}",
27
28
  "downloadedTracksCount": "Downloaded tracks: {{num}}",
28
29
  "downloadFailed": "Download Failed",
29
30
  "downloading": "Downloading",
30
31
  "downloadPlaylist": "Download Playlist",
31
- "downloadSinglesAndEps": "Download singles and eps",
32
+ "downloadSinglesAndEps": "Singles and EPs",
32
33
  "duration": "Duration",
33
34
  "edit": "Edit",
34
35
  "errors": "Errors",
@@ -47,6 +48,7 @@
47
48
  "formatScopeTab": "Different for each tab",
48
49
  "foundDeletedOrPrivateMedia": "Encountered some deleted or private media. They were skipped and won't be show in results.",
49
50
  "from": "from",
51
+ "fromYear": "From (year)",
50
52
  "invalidTemplateKeys": "Invalid template keys: {{invalidKeys}}",
51
53
  "langName": "English",
52
54
  "loadFromFile": "Load from file",
@@ -74,11 +76,13 @@
74
76
  "playlistOutputTemplate": "Output template (playlists)",
75
77
  "playlists": "Playlists",
76
78
  "reading": "Reading",
79
+ "releaseDate": "Release Date",
77
80
  "releaseYear": "Release Year",
78
81
  "resolution": "Quality",
79
82
  "retry": "Retry",
80
83
  "selectArtist": "Select artists",
81
84
  "selectInputMode": "Input mode",
85
+ "showAdvancedSearchOptions": "Advanced Search Options",
82
86
  "showBrowser": "Show web browser",
83
87
  "songOrSongs": "Song title/titles",
84
88
  "songs": "Songs",
@@ -92,6 +96,7 @@
92
96
  "totalTracksCount": "Total tracks: {{num}}",
93
97
  "trackOutputTemplate": "Output template (audio tracks)",
94
98
  "tracks": "Tracks",
99
+ "untilYear": "Until (year)",
95
100
  "update": "Update",
96
101
  "videoOutputTemplate": "Output template (videos)",
97
102
  "warnings": "Warnings",
@@ -146,7 +146,7 @@
146
146
  "tabsOrderContent": "Rosnąca lub malejąca kolejność.",
147
147
  "editTrackHeader": "Edycja danych ścieżki",
148
148
  "editTrackContent": "Pozwala na edycję danych ścieżki takich jak tytuł.",
149
- "downloadSinglesAndEpsHeader": "Pobieraj single i epki",
149
+ "downloadSinglesAndEpsHeader": "Single i epki",
150
150
  "downloadSinglesAndEpsContent": "Uwzględnij single i epki podczas pobierania dyskografii artysty.",
151
151
  "tabsOrderFieldHeader": "Atrybut sortowania zakładek",
152
152
  "tabsOrderFieldContent": "Określa atrybut używany do sortowania zakładek.",
@@ -163,5 +163,17 @@
163
163
  "ytdlpVersionHeader": "Informacje o bibliotece yt-dlp",
164
164
  "ytdlpVersionContent": "Wyświetla wersję biblioteki yt-dlp i umożliwia jej aktualizację.",
165
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ą)."
166
+ "chromeExecutablePathContent": "Ścieżka do pliku wykonywalnego chrome (pozostaw puste aby użyć chromium dostarczonego razem z aplikacją).",
167
+ "showAdvancedSearchOptionsHeader": "Zaawansowane opcje wyszukiwania",
168
+ "showAdvancedSearchOptionsContent": "Panel zaawansowanych opcji wyszukiwania.",
169
+ "downloadReleaseTypeHeader": "Rodzaj pobieranych elementów",
170
+ "downloadReleaseTypeContent": "Określa rodzaj elementów, które będą pobrane.",
171
+ "downloadReleaseDateHeader": "Data wydania",
172
+ "downloadReleaseDateContent": "Określa przedział lat dla daty wydania elementów, które będą pobrane.",
173
+ "downloadReleaseDateFromHeader": "Od",
174
+ "downloadReleaseDateFromContent": "Elementy z datą wydania wcześniejszą niż ten rok zostaną pominięte.",
175
+ "downloadReleaseDateUntilHeader": "Do",
176
+ "downloadReleaseDateUntilContent": "Elementy z datą wydania późniejszą niż ten rok zostaną pominięte.",
177
+ "downloadAlbumsHeader": "Albumy",
178
+ "downloadAlbumsContent": "Uwzględnij pełne wydania albumów podczas pobierania dyskografii artysty."
167
179
  }
@@ -22,13 +22,14 @@
22
22
  "detailsModalTitle": "Edycja danych",
23
23
  "done": "Gotowe",
24
24
  "download": "Pobierz",
25
+ "downloadAlbums": "Albumy",
25
26
  "downloadAll": "Pobierz wszystko",
26
27
  "downloadedPlaylistsCount": "Pobrane playlisty: {{num}}",
27
28
  "downloadedTracksCount": "Pobrane utwory: {{num}}",
28
29
  "downloadFailed": "Pobierz błędne",
29
30
  "downloading": "Pobieranie",
30
31
  "downloadPlaylist": "Pobierz playlistę",
31
- "downloadSinglesAndEps": "Pobieraj single i epki",
32
+ "downloadSinglesAndEps": "Single i epki",
32
33
  "duration": "Czas trwania",
33
34
  "edit": "Edytuj",
34
35
  "errors": "Błędy",
@@ -47,6 +48,7 @@
47
48
  "formatScopeTab": "Oddzielny dla każdej zakładki",
48
49
  "foundDeletedOrPrivateMedia": "Napotkano usunięte lub prywatne media. Zostały one pominięte i nie będą wyświetlane w wynikach.",
49
50
  "from": "start",
51
+ "fromYear": "Od (rok)",
50
52
  "invalidTemplateKeys": "Nieprawidłowe wartości: {{invalidKeys}}",
51
53
  "langName": "Polski",
52
54
  "loadFromFile": "Wczytaj z pliku",
@@ -74,11 +76,13 @@
74
76
  "playlistOutputTemplate": "Szablon wyjściowy (playlisty)",
75
77
  "playlists": "Playlisty",
76
78
  "reading": "Odczytywanie",
79
+ "releaseDate": "Data wydania",
77
80
  "releaseYear": "Rok wydania",
78
81
  "resolution": "Jakość",
79
82
  "retry": "Ponów",
80
83
  "selectArtist": "Wybierz wykonawcę",
81
84
  "selectInputMode": "Tryb wprowadzania",
85
+ "showAdvancedSearchOptions": "Zaawansowane opcje wyszukiwania",
82
86
  "showBrowser": "Pokaż przeglądarkę",
83
87
  "songOrSongs": "Tytuł lub tytuły utworów",
84
88
  "songs": "Utwory",
@@ -92,6 +96,7 @@
92
96
  "totalTracksCount": "Dostepne utwory: {{num}}",
93
97
  "trackOutputTemplate": "Szablon wyjściowy (pliki audio)",
94
98
  "tracks": "Ścieżki",
99
+ "untilYear": "Do (rok)",
95
100
  "update": "Aktualizuj",
96
101
  "videoOutputTemplate": "Szablon wyjściowy (pliki wideo)",
97
102
  "warnings": "Ostrzeżenia",
@@ -232,7 +232,7 @@ export const HomeView: React.FC = () => {
232
232
  }
233
233
  }, [playlists]);
234
234
 
235
- const loadInfo = (urls: string[]) => {
235
+ const loadInfo = (urls: string[], fromYear: string, untilYear: string) => {
236
236
  clear();
237
237
  setPlaylists(_map(urls, (v) => ({url: v, album: {}, tracks: []} as PlaylistInfo)));
238
238
 
@@ -244,7 +244,7 @@ export const HomeView: React.FC = () => {
244
244
  const basic = [...lists, ...vids];
245
245
 
246
246
  if (inputMode === InputMode.Artists) {
247
- loadArtists(urls);
247
+ loadArtists(urls, fromYear, untilYear);
248
248
  return;
249
249
  }
250
250
 
@@ -287,15 +287,18 @@ export const HomeView: React.FC = () => {
287
287
  }
288
288
  };
289
289
 
290
- const loadArtists = (artists: string[]) => {
290
+ const loadArtists = (artists: string[], fromYear: string, untilYear: string) => {
291
291
  const options: LaunchOptions = global.store.get("options");
292
292
  const params: GetYoutubeParams = {
293
293
  values: artists,
294
294
  lang: i18n.language,
295
295
  url: appOptions.youtubeUrl,
296
296
  options: {
297
+ downloadAlbums: appOptions.downloadAlbums,
297
298
  downloadSinglesAndEps: appOptions.downloadSinglesAndEps,
298
299
  multiMatchAction: appOptions.multiMatchAction,
300
+ fromYear,
301
+ untilYear
299
302
  },
300
303
  };
301
304
 
@@ -411,12 +414,12 @@ export const HomeView: React.FC = () => {
411
414
  }
412
415
  };
413
416
 
414
- const download = async (urls: string[]) => {
417
+ const download = async (urls: string[], fromYear?: string, untilYear?: string) => {
415
418
  const albums = _map(playlists, "album");
416
419
  const albumUrls = _map(albums, "url");
417
420
 
418
421
  if (_isEmpty(playlists) || !_difference(albumUrls, urls)) {
419
- loadInfo(urls);
422
+ loadInfo(urls, fromYear, untilYear);
420
423
  setAutoDownload(true);
421
424
  } else if (_some(albums, (album) => _some(album, v => _isNil(v)))) {
422
425
  setError(true);
@@ -853,7 +856,7 @@ export const HomeView: React.FC = () => {
853
856
  onDownloadFailed={downloadFailed}
854
857
  onLoadInfo={loadInfo}
855
858
  />
856
- <FormatSelector disabled={_isEmpty(playlists) || _isEmpty(tracks)} />
859
+ {!_isEmpty(playlists) && <FormatSelector disabled={_isEmpty(playlists) || _isEmpty(tracks)} />}
857
860
  </div>
858
861
  <Grid className={Styles.content} container spacing={2} padding={2}>
859
862
  {error && <Alert className={Styles.error} severity="error">{t("missingMediaInfoError")}</Alert>}
@@ -17,8 +17,9 @@ import versionInfo from "win-version-info";
17
17
  import NorthIcon from "@mui/icons-material/North";
18
18
  import SouthIcon from "@mui/icons-material/South";
19
19
  import {
20
- Box, Button, FormControl, FormControlLabel, FormLabel, Grid, InputLabel, MenuItem, Paper, Radio,
21
- RadioGroup, Select, SelectChangeEvent, Stack, Switch, TextField, Typography
20
+ Box, Button, Checkbox, FormControl, FormControlLabel, FormGroup, FormLabel, Grid, InputLabel,
21
+ MenuItem, Paper, Radio, RadioGroup, Select, SelectChangeEvent, Stack, Switch, TextField,
22
+ Typography
22
23
  } from "@mui/material";
23
24
 
24
25
  import {getBinPath} from "../../common/FileSystem";
@@ -159,6 +160,10 @@ export const SettingsView: React.FC = () => {
159
160
  setApplicationOptions((prev) => ({...prev, downloadSinglesAndEps: e.target.checked}));
160
161
  };
161
162
 
163
+ const onDownloadAlbumsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
164
+ setApplicationOptions((prev) => ({...prev, downloadAlbums: e.target.checked}));
165
+ };
166
+
162
167
  const onOverwriteChange = (e: React.ChangeEvent<HTMLInputElement>) => {
163
168
  setApplicationOptions((prev) => ({...prev, alwaysOverwrite: e.target.checked}));
164
169
  };
@@ -188,7 +193,7 @@ export const SettingsView: React.FC = () => {
188
193
  setYtDlpVersion(info.FileVersion);
189
194
  };
190
195
 
191
- const onUpdateYtDlpClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
196
+ const onUpdateYtDlpClick = async () => {
192
197
  const child = spawn(global.store.get("application.ytdlpExecutablePath") || `${getBinPath()}/yt-dlp.exe`, ["-U"], {shell: true});
193
198
 
194
199
  setUpdatingYtDlp(true);
@@ -285,8 +290,14 @@ export const SettingsView: React.FC = () => {
285
290
  </Grid>
286
291
  }
287
292
  </Grid>
288
- <Grid size={12} data-help="downloadSinglesAndEps">
289
- <FormControlLabel control={<Switch checked={applicationOptions.downloadSinglesAndEps} onChange={onDownloadSinglesAndEpsChange} />} label={t("downloadSinglesAndEps")} />
293
+ <Grid size={12} data-help="downloadReleaseType">
294
+ <FormControl>
295
+ <FormLabel component="legend">{t("download")}</FormLabel>
296
+ <FormGroup row>
297
+ <FormControlLabel data-help="downloadAlbums" control={<Checkbox checked={applicationOptions.downloadAlbums} onChange={onDownloadAlbumsChange} />} label={t("downloadAlbums")} />
298
+ <FormControlLabel data-help="downloadSinglesAndEps" control={<Checkbox checked={applicationOptions.downloadSinglesAndEps} onChange={onDownloadSinglesAndEpsChange} />} label={t("downloadSinglesAndEps")} />
299
+ </FormGroup>
300
+ </FormControl>
290
301
  </Grid>
291
302
  <Grid size={12} data-help="alwaysOverwrite">
292
303
  <FormControlLabel control={<Switch checked={applicationOptions.alwaysOverwrite} onChange={onOverwriteChange} />} label={t("alwaysOverwrite")} />