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 +24 -2
- package/src/automations/Selectors.ts +8 -0
- package/src/automations/YoutubeArtists.ts +20 -8
- package/src/common/Messaging.ts +1 -0
- package/src/common/Store.ts +10 -0
- package/src/common/YtdplUtils.ts +1 -0
- package/src/components/youtube/formatSelector/FormatSelector.tsx +4 -4
- package/src/components/youtube/inputPanel/InputPanel.styl +35 -0
- package/src/components/youtube/inputPanel/InputPanel.tsx +110 -29
- package/src/components/youtube/playlistTabs/PlaylistTabs.styl +1 -0
- package/src/components/youtube/playlistTabs/PlaylistTabs.tsx +1 -2
- package/src/resources/locales/de-DE/help.json +14 -2
- package/src/resources/locales/de-DE/translation.json +6 -1
- package/src/resources/locales/en-GB/help.json +14 -2
- package/src/resources/locales/en-GB/translation.json +6 -1
- package/src/resources/locales/pl-PL/help.json +14 -2
- package/src/resources/locales/pl-PL/translation.json +6 -1
- package/src/views/home/HomeView.tsx +9 -6
- package/src/views/settings/SettingsView.tsx +16 -5
package/package.json
CHANGED
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yt-grabber",
|
|
3
|
-
"version": "1.8.
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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}`);
|
package/src/common/Messaging.ts
CHANGED
package/src/common/Store.ts
CHANGED
|
@@ -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,
|
package/src/common/YtdplUtils.ts
CHANGED
|
@@ -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=
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
-
|
|
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 [
|
|
44
|
-
const [
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
};
|
|
@@ -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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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,
|
|
21
|
-
RadioGroup, Select, SelectChangeEvent, Stack, Switch, TextField,
|
|
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 (
|
|
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="
|
|
289
|
-
<
|
|
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")} />
|