yt-grabber 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.yarnrc.yml +3 -1
- package/README.md +99 -8
- package/eslint.config.ts +60 -0
- package/package.json +67 -32
- package/public/banner.png +0 -0
- package/public/profile/cookies.json +18 -0
- package/public/screenshots/main-downloading.png +0 -0
- package/public/screenshots/main-info.png +0 -0
- package/public/screenshots/main.png +0 -0
- package/public/screenshots/settings.png +0 -0
- package/src/App.styl +49 -19
- package/src/App.tsx +31 -4
- package/src/automations/Selectors.ts +7 -0
- package/src/automations/Youtube.ts +138 -0
- package/src/common/FileSystem.ts +5 -168
- package/src/common/Formatters.ts +25 -0
- package/src/common/Helpers.ts +40 -254
- package/src/common/Media.ts +28 -0
- package/src/common/Messaging.ts +38 -0
- package/src/common/Promise.ts +14 -0
- package/src/common/PuppeteerOptions.ts +3 -2
- package/src/common/Reporter.ts +61 -0
- package/src/common/Store.ts +26 -26
- package/src/common/Youtube.ts +35 -5
- package/src/common/YtdplUtils.ts +158 -0
- package/src/components/appBar/AppBar.styl +42 -17
- package/src/components/appBar/AppBar.tsx +38 -12
- package/src/components/fileField/FileField.tsx +25 -81
- package/src/components/languagePicker/LanguagePicker.styl +36 -36
- package/src/components/languagePicker/LanguagePicker.tsx +11 -11
- package/src/components/modals/{DetailsModal.styl → detailsModal/DetailsModal.styl} +1 -3
- package/src/components/modals/{DetailsModal.tsx → detailsModal/DetailsModal.tsx} +3 -7
- package/src/components/modals/imageModal/ImageModal.styl +25 -0
- package/src/components/modals/imageModal/ImageModal.tsx +72 -0
- package/src/components/numberField/NumberField.styl +14 -12
- package/src/components/progress/Progress.tsx +15 -6
- package/src/components/themePicker/ThemePicker.styl +19 -19
- package/src/components/themePicker/ThemePicker.tsx +11 -1
- package/src/components/youtube/formatSelector/FormatSelector.tsx +61 -43
- package/src/components/youtube/infoBar/InfoBar.styl +41 -0
- package/src/components/youtube/infoBar/InfoBar.tsx +83 -0
- package/src/components/youtube/inputPanel/InputPanel.styl +13 -1
- package/src/components/youtube/inputPanel/InputPanel.tsx +146 -116
- package/src/components/youtube/logMenu/LogMenu.styl +23 -0
- package/src/components/youtube/logMenu/LogMenu.tsx +135 -0
- package/src/components/youtube/mediaInfoPanel/MediaInfoPanel.styl +90 -80
- package/src/components/youtube/mediaInfoPanel/MediaInfoPanel.tsx +100 -19
- package/src/components/youtube/playlistTabs/PlaylistTabs.styl +67 -0
- package/src/components/youtube/playlistTabs/PlaylistTabs.tsx +238 -0
- package/src/components/youtube/trackList/TrackList.styl +11 -7
- package/src/components/youtube/trackList/TrackList.tsx +114 -39
- package/src/hooks/useHelp.ts +153 -0
- package/src/hooks/useWindowUpdater.ts +24 -0
- package/src/i18next.ts +23 -4
- package/src/index.ts +99 -6
- package/src/react/actions/AppActions.ts +3 -16
- package/src/react/contexts/AppContext.tsx +1 -6
- package/src/react/contexts/DataContext.tsx +90 -15
- package/src/react/reducers/AppReducer.tsx +2 -18
- package/src/react/states/AppState.ts +2 -11
- package/src/resources/bin/yt-dlp.exe +0 -0
- package/src/resources/icons/logo.ico +0 -0
- package/src/resources/locales/de-DE/help.json +134 -0
- package/src/resources/locales/de-DE/translation.json +36 -7
- package/src/resources/locales/en-GB/help.json +134 -0
- package/src/resources/locales/en-GB/translation.json +34 -5
- package/src/resources/locales/pl-PL/help.json +134 -0
- package/src/resources/locales/pl-PL/translation.json +37 -8
- package/src/styles/MaterialThemes.ts +1 -3
- package/src/views/development/DevelopmentView.styl +5 -0
- package/src/views/development/DevelopmentView.tsx +32 -3
- package/src/views/home/HomeView.styl +12 -4
- package/src/views/home/HomeView.tsx +387 -340
- package/src/views/settings/SettingsView.styl +10 -0
- package/src/views/settings/SettingsView.tsx +54 -22
- package/tests/TestMultipleMock.ts +91779 -0
- package/webpack.config.ts +14 -10
- package/public/screenshots/cutting.png +0 -0
- package/public/screenshots/downloading.png +0 -0
- package/public/screenshots/editing.png +0 -0
- package/public/screenshots/errors.png +0 -0
- package/public/screenshots/tracklist.png +0 -0
- package/src/common/Mappings.ts +0 -14
- package/src/common/Selectors.ts +0 -21
- package/src/components/directoryPicker/DirectoryPicker.tsx +0 -44
- package/src/components/splitButton/SplitButton.styl +0 -0
- package/src/components/splitButton/SplitButton.tsx +0 -125
- package/src/components/themeSwitcher/ThemeSwitcher.styl +0 -10
- package/src/components/themeSwitcher/ThemeSwitcher.tsx +0 -43
- package/src/enums/DataResponse.ts +0 -5
- package/src/enums/Media.ts +0 -16
- package/src/enums/MediaFormat.ts +0 -10
- package/src/enums/MimeTypes.ts +0 -14
- package/src/hooks/useData.ts +0 -61
- package/src/react/contexts/DataContext copy.tsx +0 -76
- package/src/resources/images/logo.png +0 -0
- package/src/tests/MissingDetailsTracksMock.ts +0 -7737
- /package/{src/tests/CompleteTracksMock.ts → tests/TestPlaylistMock.ts} +0 -0
package/.yarnrc.yml
CHANGED
package/README.md
CHANGED
|
@@ -1,11 +1,102 @@
|
|
|
1
|
-
|
|
2
|
-
`yarn install`
|
|
1
|
+
<img src="public/banner.png" alt="YT Grabber Banner" width="800">
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
`yarn build`
|
|
3
|
+
---
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
`yarn start`
|
|
5
|
+
**YT Grabber** is a robust desktop application designed to retrieve multimedia from YouTube and YouTube Music services.
|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
It provides responsive UI to manage your downloads and automation features improve and accelerate download process.
|
|
8
|
+
|
|
9
|
+
It provides support for downloading:
|
|
10
|
+
|
|
11
|
+
- videos
|
|
12
|
+
- audio tracks
|
|
13
|
+
- complete playlists
|
|
14
|
+
- full discographies
|
|
15
|
+
|
|
16
|
+
Various formats and quality options are available for both - audio and video.
|
|
17
|
+
|
|
18
|
+
Each download can be customized to your needs for easy workflow automation.
|
|
19
|
+
|
|
20
|
+
## Table of Contents
|
|
21
|
+
|
|
22
|
+
- [Table of Contents](#table-of-contents)
|
|
23
|
+
- [Features](#features)
|
|
24
|
+
- [Screenshots](#screenshots)
|
|
25
|
+
- [Usage](#usage)
|
|
26
|
+
- [Development](#development)
|
|
27
|
+
- [Running](#running)
|
|
28
|
+
- [Packaging](#packaging)
|
|
29
|
+
- [License](#license)
|
|
30
|
+
- [Legal Disclaimer](#legal-disclaimer)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
* Download video, audio, playlists and complete artist discographies
|
|
36
|
+
|
|
37
|
+
* Multiple output formats (mp3, m4u, flac, wav, mp4, mkv)
|
|
38
|
+
|
|
39
|
+
* Customizable audio and video quality
|
|
40
|
+
|
|
41
|
+
* Trimming video and audio
|
|
42
|
+
|
|
43
|
+
* Batch multimedia download
|
|
44
|
+
|
|
45
|
+
* Metadata (tags) embedding
|
|
46
|
+
|
|
47
|
+
* Configurable output
|
|
48
|
+
|
|
49
|
+
* Multiple language support (English, German, Polish out of the box)
|
|
50
|
+
|
|
51
|
+
* Light/Dark theme
|
|
52
|
+
|
|
53
|
+
* Responsive and clean UI
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
## Screenshots
|
|
57
|
+
|
|
58
|
+
<img src="public/screenshots/main.png" alt="Main" width="600">
|
|
59
|
+
|
|
60
|
+
*Main window*
|
|
61
|
+
|
|
62
|
+
<img src="public/screenshots/main-info.png" alt="Main: Loading Info" width="600">
|
|
63
|
+
|
|
64
|
+
*Displaying multimedia info*
|
|
65
|
+
|
|
66
|
+
<img src="public/screenshots/main-downloading.png" alt="Main: Downloading Files" width="600">
|
|
67
|
+
|
|
68
|
+
*Downloading multimedia*
|
|
69
|
+
|
|
70
|
+
<img src="public/screenshots/settings.png" alt="Settings" width="600">
|
|
71
|
+
|
|
72
|
+
*Settings*
|
|
73
|
+
|
|
74
|
+
## Usage
|
|
75
|
+
|
|
76
|
+
Download and run latest release installer from [here](https://github.com/karenpommeroy/yt-grabber/releases).
|
|
77
|
+
|
|
78
|
+
## Development
|
|
79
|
+
|
|
80
|
+
To build **yt-grabber** follow these steps:
|
|
81
|
+
|
|
82
|
+
1. Clone this repository
|
|
83
|
+
2. Install dependencies using `npm install` or `yarn install` command
|
|
84
|
+
3. If using `yarn` with Visual Studio Code also run `yarn dlx @yarnpkg/sdks vscode`
|
|
85
|
+
4. Run `npm build` or `yarn build` command to build
|
|
86
|
+
|
|
87
|
+
## Running
|
|
88
|
+
|
|
89
|
+
Run `npm start` or `yarn start` to run the app for development.
|
|
90
|
+
This will start webpack development server that will watch for changes to source code and reload the application automatically.
|
|
91
|
+
|
|
92
|
+
## Packaging
|
|
93
|
+
|
|
94
|
+
To prepare release application package run `npm dist` or `yarn dist` command.
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
99
|
+
|
|
100
|
+
## Legal Disclaimer
|
|
101
|
+
|
|
102
|
+
All music files downloaded through this software must be legally owned and purchased by the user. By downloading music via this software, you represent that you have purchased and fully own the rights to any downloaded content or an active subscription to YouTube Music. Downloading or distributing pirated or illegal music copies is strictly prohibited. I claim no ownership rights to any downloaded music files - all such rights remain with the content owner. I accept no liability for the illegal use of any files downloaded through this software.
|
package/eslint.config.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import react from "eslint-plugin-react";
|
|
2
|
+
import {defineConfig} from "eslint/config";
|
|
3
|
+
import globals from "globals";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import {fileURLToPath} from "node:url";
|
|
6
|
+
|
|
7
|
+
import {FlatCompat} from "@eslint/eslintrc";
|
|
8
|
+
import js from "@eslint/js";
|
|
9
|
+
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
|
10
|
+
import tsParser from "@typescript-eslint/parser";
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
const compat = new FlatCompat({
|
|
15
|
+
baseDirectory: __dirname,
|
|
16
|
+
recommendedConfig: js.configs.recommended,
|
|
17
|
+
allConfig: js.configs.all
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export default defineConfig([{
|
|
21
|
+
extends: compat.extends(
|
|
22
|
+
"eslint:recommended",
|
|
23
|
+
"plugin:react/recommended",
|
|
24
|
+
"plugin:@typescript-eslint/recommended",
|
|
25
|
+
"prettier",
|
|
26
|
+
),
|
|
27
|
+
|
|
28
|
+
plugins: {
|
|
29
|
+
react,
|
|
30
|
+
"@typescript-eslint": typescriptEslint as any,
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
languageOptions: {
|
|
34
|
+
globals: {
|
|
35
|
+
...globals.browser,
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
parser: tsParser,
|
|
39
|
+
ecmaVersion: "latest",
|
|
40
|
+
sourceType: "module",
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
settings: {
|
|
44
|
+
react: {
|
|
45
|
+
version: "detect",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
rules: {
|
|
50
|
+
indent: ["error", 4],
|
|
51
|
+
"linebreak-style": ["error", "windows"],
|
|
52
|
+
quotes: ["error", "double"],
|
|
53
|
+
semi: ["error", "always"],
|
|
54
|
+
"@typescript-eslint/no-var-requires": 0,
|
|
55
|
+
"@typescript-eslint/no-explicit-any": 0,
|
|
56
|
+
"@typescript-eslint/no-empty-interface": 0,
|
|
57
|
+
"react/prop-types": 0,
|
|
58
|
+
"react/display-name": 0,
|
|
59
|
+
},
|
|
60
|
+
}]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yt-grabber",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Youtube Grabber",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"repository": {
|
|
@@ -9,28 +9,56 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"clean": "rimraf dist",
|
|
11
11
|
"start": "concurrently \"cross-env NODE_ENV=development yarn run watch\" \"yarn run electron\"",
|
|
12
|
-
"build": "cross-env NODE_ENV=development
|
|
13
|
-
"build:prod": "cross-env NODE_ENV=production
|
|
14
|
-
"electron": "wait-on dist/index.js &&
|
|
12
|
+
"build": "yarn clean && \"cross-env NODE_ENV=development webpack\" --config webpack.config.ts",
|
|
13
|
+
"build:prod": "yarn clean && \"cross-env NODE_ENV=production webpack\" --config webpack.config.ts",
|
|
14
|
+
"electron": "wait-on dist/index.js && ELECTRON_DISABLE_SECURITY_WARNINGS=true electron .",
|
|
15
15
|
"watch": "webpack --watch --config webpack.config.ts",
|
|
16
16
|
"pack": "yarn build:prod && electron-builder --dir",
|
|
17
17
|
"dist": "yarn build:prod && electron-builder"
|
|
18
18
|
},
|
|
19
19
|
"build": {
|
|
20
20
|
"appId": "yt.grabber",
|
|
21
|
+
"productName": "YT Grabber",
|
|
22
|
+
"copyright": "© 2025 Marcin Karpiński",
|
|
21
23
|
"files": [
|
|
22
24
|
"dist/**/*",
|
|
23
25
|
"package.json"
|
|
24
26
|
],
|
|
27
|
+
"asar": true,
|
|
28
|
+
"win": {
|
|
29
|
+
"target": "nsis",
|
|
30
|
+
"icon": "dist/resources/icons/logo.ico",
|
|
31
|
+
"artifactName": "${productName}-setup-${version}.${ext}"
|
|
32
|
+
},
|
|
33
|
+
"nsis": {
|
|
34
|
+
"oneClick": false,
|
|
35
|
+
"allowToChangeInstallationDirectory": true,
|
|
36
|
+
"installerIcon": "dist/resources/icons/logo.ico",
|
|
37
|
+
"uninstallerIcon": "dist/resources/icons/logo.ico",
|
|
38
|
+
"installerHeaderIcon": "dist/resources/icons/logo.ico",
|
|
39
|
+
"createDesktopShortcut": true,
|
|
40
|
+
"createStartMenuShortcut": true,
|
|
41
|
+
"shortcutName": "YT Grabber"
|
|
42
|
+
},
|
|
25
43
|
"directories": {
|
|
26
44
|
"output": "release"
|
|
27
45
|
},
|
|
28
46
|
"extraResources": [
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
47
|
+
{
|
|
48
|
+
"from": "dist/resources/bin",
|
|
49
|
+
"to": "bin",
|
|
50
|
+
"filter": [
|
|
51
|
+
"*.exe"
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"from": "dist/resources/locales",
|
|
56
|
+
"to": "locales"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"from": "public/profile",
|
|
60
|
+
"to": "profile"
|
|
61
|
+
}
|
|
34
62
|
]
|
|
35
63
|
},
|
|
36
64
|
"author": "Marcin Karpiński <mkarpins@gmail.com>",
|
|
@@ -38,10 +66,13 @@
|
|
|
38
66
|
"dependencies": {
|
|
39
67
|
"@emotion/react": "^11.14.0",
|
|
40
68
|
"@emotion/styled": "^11.14.0",
|
|
41
|
-
"@mui/icons-material": "^
|
|
42
|
-
"@mui/
|
|
69
|
+
"@mui/icons-material": "^7.0.0",
|
|
70
|
+
"@mui/lab": "^7.0.0-beta.9",
|
|
71
|
+
"@mui/material": "^7.0.0",
|
|
72
|
+
"axios": "^1.8.4",
|
|
43
73
|
"classnames": "^2.5.1",
|
|
44
74
|
"electron-devtools-installer": "^4.0.0",
|
|
75
|
+
"electron-reload": "^2.0.0-alpha.1",
|
|
45
76
|
"electron-store": "^8.2.0",
|
|
46
77
|
"fs-extra": "^11.3.0",
|
|
47
78
|
"i18next": "^24.2.2",
|
|
@@ -51,17 +82,18 @@
|
|
|
51
82
|
"mkdirp": "^3.0.1",
|
|
52
83
|
"moment": "^2.30.1",
|
|
53
84
|
"moment-duration-format": "^2.3.2",
|
|
54
|
-
"
|
|
55
|
-
"puppeteer
|
|
85
|
+
"mui-image": "^1.0.7",
|
|
86
|
+
"puppeteer": "^24.4.0",
|
|
87
|
+
"puppeteer-core": "^24.4.0",
|
|
56
88
|
"puppeteer-extra": "^3.3.6",
|
|
57
89
|
"puppeteer-extra-plugin-devtools": "^2.4.6",
|
|
58
90
|
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
|
59
91
|
"react": "^19.0.0",
|
|
60
92
|
"react-dom": "^19.0.0",
|
|
61
|
-
"react-i18next": "^15.4.
|
|
93
|
+
"react-i18next": "^15.4.1",
|
|
62
94
|
"react-number-format": "^5.4.3",
|
|
63
|
-
"react-router-dom": "^7.
|
|
64
|
-
"usehooks-ts": "^3.1.
|
|
95
|
+
"react-router-dom": "^7.4.0",
|
|
96
|
+
"usehooks-ts": "^3.1.1",
|
|
65
97
|
"yt-dlp-wrap": "^2.3.12"
|
|
66
98
|
},
|
|
67
99
|
"devDependencies": {
|
|
@@ -70,32 +102,35 @@
|
|
|
70
102
|
"@types/i18next": "^13.0.0",
|
|
71
103
|
"@types/i18next-node-fs-backend": "^2.1.5",
|
|
72
104
|
"@types/jsonschema": "^1.1.1",
|
|
73
|
-
"@types/lodash": "^4.17.
|
|
105
|
+
"@types/lodash": "^4.17.16",
|
|
74
106
|
"@types/moment-duration-format": "^2.2.6",
|
|
75
|
-
"@types/
|
|
107
|
+
"@types/mui-image": "^1.0.5",
|
|
108
|
+
"@types/node": "^22.13.14",
|
|
76
109
|
"@types/prettier": "^3.0.0",
|
|
77
110
|
"@types/puppeteer-core": "^7.0.4",
|
|
78
|
-
"@types/react": "^19.0.
|
|
79
|
-
"@types/react-dom": "^19.0.
|
|
111
|
+
"@types/react": "^19.0.12",
|
|
112
|
+
"@types/react-dom": "^19.0.4",
|
|
80
113
|
"@types/webpack-dev-server": "^4.7.2",
|
|
81
114
|
"@types/webpack-env": "^1.18.8",
|
|
82
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
83
|
-
"@typescript-eslint/parser": "^8.
|
|
115
|
+
"@typescript-eslint/eslint-plugin": "^8.28.0",
|
|
116
|
+
"@typescript-eslint/parser": "^8.28.0",
|
|
84
117
|
"concurrently": "^9.1.2",
|
|
85
|
-
"copy-webpack-plugin": "^
|
|
118
|
+
"copy-webpack-plugin": "^13.0.0",
|
|
86
119
|
"cross-env": "^7.0.3",
|
|
87
120
|
"css-loader": "^7.1.2",
|
|
88
|
-
"electron": "^
|
|
89
|
-
"electron-builder": "^
|
|
90
|
-
"eslint": "^9.
|
|
91
|
-
"eslint-config-prettier": "^10.
|
|
121
|
+
"electron": "^35.1.2",
|
|
122
|
+
"electron-builder": "^26.0.12",
|
|
123
|
+
"eslint": "^9.23.0",
|
|
124
|
+
"eslint-config-prettier": "^10.1.1",
|
|
92
125
|
"eslint-plugin-css": "^0.11.0",
|
|
93
126
|
"eslint-plugin-react": "^7.37.4",
|
|
94
|
-
"eslint-webpack-plugin": "^
|
|
127
|
+
"eslint-webpack-plugin": "^5.0.0",
|
|
128
|
+
"globals": "^16.0.0",
|
|
95
129
|
"html-webpack-plugin": "^5.6.3",
|
|
96
130
|
"i18next-scanner-webpack": "^0.9.1",
|
|
131
|
+
"jiti": "^2.4.2",
|
|
97
132
|
"mini-css-extract-plugin": "^2.9.2",
|
|
98
|
-
"prettier": "^3.
|
|
133
|
+
"prettier": "^3.5.3",
|
|
99
134
|
"raw-loader": "^4.0.2",
|
|
100
135
|
"rimraf": "^6.0.1",
|
|
101
136
|
"source-map-loader": "^5.0.0",
|
|
@@ -104,11 +139,11 @@
|
|
|
104
139
|
"ts-loader": "^9.5.2",
|
|
105
140
|
"ts-node": "^10.9.2",
|
|
106
141
|
"ts-node-dev": "^2.0.0",
|
|
107
|
-
"typescript": "^5.
|
|
142
|
+
"typescript": "^5.8.2",
|
|
108
143
|
"wait-on": "^8.0.2",
|
|
109
|
-
"webpack": "^5.
|
|
144
|
+
"webpack": "^5.98.0",
|
|
110
145
|
"webpack-cli": "^6.0.1",
|
|
111
|
-
"webpack-dev-server": "^5.2.
|
|
146
|
+
"webpack-dev-server": "^5.2.1",
|
|
112
147
|
"webpack-node-externals": "^3.0.0"
|
|
113
148
|
},
|
|
114
149
|
"packageManager": "yarn@4.4.1"
|
|
Binary file
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "SOCS",
|
|
4
|
+
"value": "CAISNQgREitib3FfaWRlbnRpdHlmcm9udGVuZHVpc2VydmVyXzIwMjUwMzI2LjA4X3AwGgJwbCACGgYIgKqSvwY",
|
|
5
|
+
"domain": ".youtube.com",
|
|
6
|
+
"path": "/",
|
|
7
|
+
"expires": 2298668700.829343,
|
|
8
|
+
"size": 91,
|
|
9
|
+
"httpOnly": false,
|
|
10
|
+
"secure": true,
|
|
11
|
+
"session": false,
|
|
12
|
+
"sameSite": "Lax",
|
|
13
|
+
"priority": "Medium",
|
|
14
|
+
"sameParty": false,
|
|
15
|
+
"sourceScheme": "Secure",
|
|
16
|
+
"sourcePort": 443
|
|
17
|
+
}
|
|
18
|
+
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/App.styl
CHANGED
|
@@ -1,24 +1,54 @@
|
|
|
1
1
|
@import "./styles/mixins.styl"
|
|
2
2
|
@import "./styles/fonts.styl"
|
|
3
3
|
|
|
4
|
-
html
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
html {
|
|
5
|
+
font-size: 1rem;
|
|
6
|
+
font-family: "Lato";
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
body {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
no-select();
|
|
12
|
+
|
|
13
|
+
:global(#root) {
|
|
14
|
+
position: absolute;
|
|
15
|
+
width: 100%;
|
|
16
|
+
height: 100%;
|
|
17
|
+
overflow: hidden;
|
|
18
|
+
}
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
.app {
|
|
21
|
+
background-color: var(--theme-palette-background-paper);
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
height: 100vh;
|
|
25
|
+
width: 100vw;
|
|
26
|
+
|
|
27
|
+
&.help {
|
|
28
|
+
cursor: help !important;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.help-popup {
|
|
33
|
+
z-index: 9999999;
|
|
34
|
+
padding: 12px;
|
|
35
|
+
|
|
36
|
+
.header {
|
|
37
|
+
font-weight: bold;
|
|
38
|
+
margin-bottom: 8px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.content {
|
|
42
|
+
p {
|
|
43
|
+
&:first-child {
|
|
44
|
+
margin-top: 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
&:last-child {
|
|
48
|
+
margin-bottom: 0;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/App.tsx
CHANGED
|
@@ -1,28 +1,55 @@
|
|
|
1
|
+
import classnames from "classnames";
|
|
2
|
+
import _isArray from "lodash/isArray";
|
|
3
|
+
import _map from "lodash/map";
|
|
4
|
+
import _split from "lodash/split";
|
|
1
5
|
import React from "react";
|
|
2
6
|
import {HashRouter, Route, Routes} from "react-router-dom";
|
|
3
7
|
|
|
4
|
-
import {Box, CssBaseline} from "@mui/material";
|
|
8
|
+
import {Box, CssBaseline, Paper, Popper, Typography} from "@mui/material";
|
|
5
9
|
|
|
6
10
|
import Styles from "./App.styl";
|
|
7
11
|
import AppBar from "./components/appBar/AppBar";
|
|
12
|
+
import useHelp from "./hooks/useHelp";
|
|
8
13
|
import {useAppContext} from "./react/contexts/AppContext";
|
|
9
14
|
import DevelopmentView from "./views/development/DevelopmentView";
|
|
10
15
|
import {HomeView} from "./views/home/HomeView";
|
|
11
16
|
import SettingsView from "./views/settings/SettingsView";
|
|
12
17
|
|
|
13
18
|
export const App: React.FC = () => {
|
|
14
|
-
const {
|
|
19
|
+
const {state} = useAppContext();
|
|
20
|
+
const {anchorEl, help} = useHelp();
|
|
21
|
+
|
|
22
|
+
const renderLineBreaks = (value: string) => {
|
|
23
|
+
return _map(_split(value, "\n"), (v, k) => <React.Fragment key={k}>{v}<br /></React.Fragment>);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const renderText = (value: string | string[]) => {
|
|
27
|
+
const valueArray = _isArray(value) ? value : [value];
|
|
28
|
+
|
|
29
|
+
return _map(valueArray, (v, k) => <p key={k}>{renderLineBreaks(v)}</p>);
|
|
30
|
+
};
|
|
15
31
|
|
|
16
32
|
return (
|
|
17
33
|
<HashRouter>
|
|
18
|
-
<Box className={Styles.app}>
|
|
34
|
+
<Box className={classnames(Styles.app, {[Styles.help]: state.help})}>
|
|
19
35
|
<CssBaseline enableColorScheme />
|
|
20
|
-
<AppBar />
|
|
36
|
+
<AppBar disableNavigation={state.loading} />
|
|
21
37
|
<Routes location={state.location}>
|
|
22
38
|
<Route path="/" element={<HomeView />} />
|
|
23
39
|
<Route path="/settings" element={<SettingsView />} />
|
|
24
40
|
<Route path="/development" element={<DevelopmentView />} />
|
|
25
41
|
</Routes>
|
|
42
|
+
<Popper
|
|
43
|
+
className={Styles.helpPopup}
|
|
44
|
+
open={Boolean(anchorEl)}
|
|
45
|
+
anchorEl={anchorEl}
|
|
46
|
+
disablePortal
|
|
47
|
+
>
|
|
48
|
+
<Paper sx={{p: 2}}>
|
|
49
|
+
<Typography className={Styles.header} color="textSecondary">{help.header}</Typography>
|
|
50
|
+
<Typography component="div" className={Styles.content}>{renderText(help.content)}</Typography>
|
|
51
|
+
</Paper>
|
|
52
|
+
</Popper>
|
|
26
53
|
</Box>
|
|
27
54
|
</HashRouter>
|
|
28
55
|
);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const AlbumsHrefSelector = "//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/a[contains(text(), 'Album')]";
|
|
2
|
+
|
|
3
|
+
export const AlbumFilterSelector = "//ytmusic-app-layout//div[@id='content']/ytmusic-browse-response//ytmusic-section-list-renderer/div[@id='header']//iron-selector//a[contains(@title, 'Album')]";
|
|
4
|
+
|
|
5
|
+
export const AlbumLinkSelector = "//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')]//a[contains(@class, 'yt-simple-endpoint') and contains(@class, 'yt-formatted-string')]";
|
|
6
|
+
|
|
7
|
+
export const AlbumsDirectLinkSelector = "//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')]//a[contains(@class, 'yt-simple-endpoint') and contains(@class, 'yt-formatted-string')]";
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import {i18n as i18next} from "i18next";
|
|
3
|
+
import _forEach from "lodash/forEach";
|
|
4
|
+
import _includes from "lodash/includes";
|
|
5
|
+
import _isEmpty from "lodash/isEmpty";
|
|
6
|
+
import _map from "lodash/map";
|
|
7
|
+
import _merge from "lodash/merge";
|
|
8
|
+
import _replace from "lodash/replace";
|
|
9
|
+
import {Browser, LaunchOptions, Page, TimeoutError} from "puppeteer";
|
|
10
|
+
import puppeteer from "puppeteer-extra";
|
|
11
|
+
import StealthPlugin from "puppeteer-extra-plugin-stealth";
|
|
12
|
+
|
|
13
|
+
import {getProfilePath} from "../common/FileSystem";
|
|
14
|
+
import {waitFor} from "../common/Helpers";
|
|
15
|
+
import {GetYoutubeUrlParams, GetYoutubeUrlResult} from "../common/Messaging";
|
|
16
|
+
import puppeteerOptions from "../common/PuppeteerOptions";
|
|
17
|
+
import {IReporter, ProgressInfo, Reporter} from "../common/Reporter";
|
|
18
|
+
import {
|
|
19
|
+
AlbumFilterSelector, AlbumLinkSelector, AlbumsDirectLinkSelector, AlbumsHrefSelector
|
|
20
|
+
} from "./Selectors";
|
|
21
|
+
|
|
22
|
+
let page: Page;
|
|
23
|
+
let browser: Browser;
|
|
24
|
+
let reporter: IReporter<GetYoutubeUrlResult>;
|
|
25
|
+
|
|
26
|
+
puppeteer.use(StealthPlugin());
|
|
27
|
+
|
|
28
|
+
const navigateToPage = async (url: string) => {
|
|
29
|
+
await page.goto(url, {
|
|
30
|
+
waitUntil: ["networkidle0", "domcontentloaded", "load"],
|
|
31
|
+
timeout: puppeteerOptions.timeout,
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const execute = async (
|
|
36
|
+
params: GetYoutubeUrlParams,
|
|
37
|
+
options: LaunchOptions,
|
|
38
|
+
i18n: i18next,
|
|
39
|
+
onProgress: (data: ProgressInfo<GetYoutubeUrlResult>) => void,
|
|
40
|
+
) => {
|
|
41
|
+
try {
|
|
42
|
+
const result: GetYoutubeUrlResult = {warnings: [], errors: [], urls: []};
|
|
43
|
+
const userAgent = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Mobile Safari/537.3";
|
|
44
|
+
|
|
45
|
+
await i18n.changeLanguage(params.lang);
|
|
46
|
+
|
|
47
|
+
reporter = new Reporter(onProgress);
|
|
48
|
+
reporter.start(i18n.t("starting"));
|
|
49
|
+
browser = await puppeteer.launch(_merge(puppeteerOptions, options));
|
|
50
|
+
[page] = await browser.pages();
|
|
51
|
+
|
|
52
|
+
await page.setUserAgent(userAgent);
|
|
53
|
+
|
|
54
|
+
const cachedCookies = fs.readJSONSync(getProfilePath() + "/cookies.json", {throws: false});
|
|
55
|
+
|
|
56
|
+
if (_isEmpty(cachedCookies)) {
|
|
57
|
+
await waitFor(3000);
|
|
58
|
+
const pageCookies = await page.cookies();
|
|
59
|
+
|
|
60
|
+
fs.writeJSONSync(getProfilePath() + "/cookies.json", pageCookies, {spaces: 2});
|
|
61
|
+
|
|
62
|
+
await page.setCookie(...pageCookies);
|
|
63
|
+
} else {
|
|
64
|
+
await page.setCookie(...cachedCookies);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await navigateToPage(params.url);
|
|
68
|
+
|
|
69
|
+
const process = async (urlToProcess: string) => {
|
|
70
|
+
const results: string[] = [];
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
await navigateToPage(urlToProcess);
|
|
74
|
+
|
|
75
|
+
const element = await page.waitForSelector(`::-p-xpath(${AlbumsHrefSelector})`, {timeout: 1000});
|
|
76
|
+
const albumsUrl = await element.evaluate((el) => el.getAttribute("href"));
|
|
77
|
+
|
|
78
|
+
await navigateToPage(`${params.url}/${albumsUrl}`);
|
|
79
|
+
const albumFilterButton = await page.waitForSelector(`::-p-xpath(${AlbumFilterSelector})`, {timeout: 1000});
|
|
80
|
+
|
|
81
|
+
albumFilterButton.click();
|
|
82
|
+
await page.waitForNetworkIdle();
|
|
83
|
+
|
|
84
|
+
// await page.screenshot({path: './profile/debug.png', fullPage: true});
|
|
85
|
+
// fs.writeFileSync("./profile/debug.html", await page.content());
|
|
86
|
+
|
|
87
|
+
const items = await page.$$eval(`xpath/${AlbumLinkSelector}`, (elements) => elements.map((el) => el.getAttribute("href")));
|
|
88
|
+
|
|
89
|
+
for (const item of items) {
|
|
90
|
+
results.push(`${params.url}/${item}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return results;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
const items = await page.$$eval(`xpath/${AlbumsDirectLinkSelector}`, (elements) => elements.map((el) => el.getAttribute("href")));
|
|
96
|
+
|
|
97
|
+
for (const item of items) {
|
|
98
|
+
results.push(`${params.url}/${item}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return results;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
for (const u of params.artistUrls) {
|
|
106
|
+
const data = await process(u);
|
|
107
|
+
|
|
108
|
+
result.urls.push(...data);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
reporter.finish("done", result);
|
|
112
|
+
} catch (error: any) {
|
|
113
|
+
const result: GetYoutubeUrlResult = {errors: []};
|
|
114
|
+
|
|
115
|
+
if (error instanceof TimeoutError) {
|
|
116
|
+
result.errors.push({title: i18n.t("exceptionTimeout"), description: i18n.t("exceptionTimeoutText")});
|
|
117
|
+
} else {
|
|
118
|
+
result.errors.push({title: i18n.t("exceptionGetYoutubeUrls"), description: i18n.t("exceptionGetYoutubeUrlsText", {error: error.name})});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
reporter.finish("done", result);
|
|
122
|
+
console.error("Execution failed at stage: ", error.stack);
|
|
123
|
+
} finally {
|
|
124
|
+
await closeResources();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const closeResources = async () => {
|
|
129
|
+
if (page) await page.close();
|
|
130
|
+
if (browser) await browser.close();
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const cancel = async () => {
|
|
134
|
+
browser.close();
|
|
135
|
+
browser.disconnect();
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export default execute;
|