mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-07 20:10:03 +03:00
Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8d2e1d890 | ||
|
|
0511996b26 | ||
|
|
f29a29bf2f | ||
|
|
24d4107311 | ||
|
|
a46f9c37c6 | ||
|
|
2b1c68bad0 | ||
|
|
e2d23404ce | ||
|
|
db208ed55e | ||
|
|
b87a9f1e2f | ||
|
|
a1ac1e450d | ||
|
|
6764ea6c3b | ||
|
|
8a52020186 | ||
|
|
695b836852 | ||
|
|
71d7c30032 | ||
|
|
5ca4f036c7 | ||
|
|
1ffe61f01f | ||
|
|
5e331b9ffa | ||
|
|
09bdae90e2 | ||
|
|
37cc8f4fe1 | ||
|
|
925e083b8d | ||
|
|
6dc42278c4 | ||
|
|
12c227badb | ||
|
|
181a9f842c | ||
|
|
b79d801c0f | ||
|
|
fc3691336d | ||
|
|
bcd879ebc8 | ||
|
|
b646db4828 | ||
|
|
426d52e359 | ||
|
|
17199dd9c0 | ||
|
|
c680c2827b | ||
|
|
2dbf8d31f7 | ||
|
|
a3753e557c | ||
|
|
ec80abdc8e | ||
|
|
1ef7d24c22 | ||
|
|
4b6f6996ae | ||
|
|
c930ee94c5 | ||
|
|
e88edbef5a | ||
|
|
ac13ed3359 | ||
|
|
faae0d44e6 | ||
|
|
7d8ec04ad6 | ||
|
|
8629e6ae9e | ||
|
|
6e311d46a6 | ||
|
|
006e983c14 | ||
|
|
5db3e06a81 | ||
|
|
2ced7b7f91 | ||
|
|
042baa418b | ||
|
|
deb928da12 | ||
|
|
a7f5cc01d3 | ||
|
|
414b6a26d9 | ||
|
|
c069672e62 | ||
|
|
167d9dafa2 | ||
|
|
9302084f60 | ||
|
|
ac0199f596 | ||
|
|
8e8ab7ac6c | ||
|
|
f06c9ba44a | ||
|
|
6e593472d9 | ||
|
|
0bddbda36d | ||
|
|
23feb05fab | ||
|
|
a0eff4d96d | ||
|
|
b87b49d77b | ||
|
|
c05026aa15 | ||
|
|
883df63d2f | ||
|
|
da1d49b541 | ||
|
|
393ed5a210 | ||
|
|
54492b109a | ||
|
|
7eac88a31f | ||
|
|
8fec9639eb | ||
|
|
a15e1f98fa | ||
|
|
6604484765 | ||
|
|
c58f8a4058 | ||
|
|
8545016f1d | ||
|
|
9b1e84821e | ||
|
|
6505fad7bc | ||
|
|
d245904c0d | ||
|
|
0095ea1271 | ||
|
|
b41d10f514 | ||
|
|
8e3d6a0af6 | ||
|
|
1e4995c5ce | ||
|
|
710e3613a8 | ||
|
|
28331c1037 | ||
|
|
202c0718b7 | ||
|
|
a985963661 | ||
|
|
f673b325fd | ||
|
|
5f4a7a1e69 | ||
|
|
d54b2a73c8 | ||
|
|
8e7bb4ba3b | ||
|
|
d595de5786 | ||
|
|
31394fa98c | ||
|
|
af595d3df8 | ||
|
|
81377a2b38 | ||
|
|
35bdd1deeb | ||
|
|
a1ec53edb9 | ||
|
|
aa130d3fc9 | ||
|
|
0f0bf3a401 | ||
|
|
73b9c61080 | ||
|
|
501806909a | ||
|
|
77dd96b3b9 | ||
|
|
505b145bb3 | ||
|
|
ba5592015d | ||
|
|
10c90a01f2 | ||
|
|
abaa799628 | ||
|
|
9e148d1464 | ||
|
|
9fa646132d | ||
|
|
f98ba00551 | ||
|
|
bc1e0ea542 | ||
|
|
9ebb684d5c | ||
|
|
91713f1140 | ||
|
|
377676cda4 | ||
|
|
8e445bb80d | ||
|
|
f0bde1efc4 | ||
|
|
4be6f341da | ||
|
|
dd62f1a7b3 | ||
|
|
1cdd4d0e15 | ||
|
|
e4e1e67855 | ||
|
|
867d2394de | ||
|
|
eb54c2fda5 | ||
|
|
deacf5a49a |
@@ -1,61 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"project": {
|
||||
"name": "youtube-dl-material"
|
||||
},
|
||||
"apps": [
|
||||
{
|
||||
"root": "src",
|
||||
"outDir": "dist",
|
||||
"assets": [
|
||||
"assets",
|
||||
"favicon.ico",
|
||||
"backend/audio",
|
||||
"backend/video",
|
||||
"backend",
|
||||
{ "glob": "default.json", "input": "./", "output": "../backend/config/", "allowOutsideOutDir": true }
|
||||
],
|
||||
"index": "index.html",
|
||||
"main": "main.ts",
|
||||
"polyfills": "polyfills.ts",
|
||||
"test": "test.ts",
|
||||
"tsconfig": "tsconfig.app.json",
|
||||
"testTsconfig": "tsconfig.spec.json",
|
||||
"prefix": "app",
|
||||
"styles": [
|
||||
"styles.css"
|
||||
],
|
||||
"scripts": [],
|
||||
"environmentSource": "environments/environment.ts",
|
||||
"environments": {
|
||||
"dev": "environments/environment.ts",
|
||||
"prod": "environments/environment.prod.ts"
|
||||
}
|
||||
}
|
||||
],
|
||||
"e2e": {
|
||||
"protractor": {
|
||||
"config": "./protractor.conf.js"
|
||||
}
|
||||
},
|
||||
"lint": [
|
||||
{
|
||||
"project": "src/tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"project": "src/tsconfig.spec.json"
|
||||
},
|
||||
{
|
||||
"project": "e2e/tsconfig.e2e.json"
|
||||
}
|
||||
],
|
||||
"test": {
|
||||
"karma": {
|
||||
"config": "./karma.conf.js"
|
||||
}
|
||||
},
|
||||
"defaults": {
|
||||
"styleExt": "css",
|
||||
"component": {}
|
||||
}
|
||||
}
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -43,6 +43,10 @@ Thumbs.db
|
||||
|
||||
node_modules/*
|
||||
backend/node_modules/*
|
||||
backend/public/*
|
||||
YoutubeDL-Material/node_modules/*
|
||||
backend/video/*
|
||||
backend/audio/*
|
||||
backend/audio/*
|
||||
backend/public/*
|
||||
backend/db.json
|
||||
src/assets/default.json
|
||||
|
||||
21
.vscode/launch.json
vendored
Normal file
21
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Attach",
|
||||
"port": 9229
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch chrome against localhost",
|
||||
"url": "http://localhost:4200",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
21
Dockerfile
Normal file
21
Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM alpine:3.11
|
||||
|
||||
RUN apk add --update npm python ffmpeg
|
||||
|
||||
# Change directory so that our commands run inside this new directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy dependency definitions
|
||||
COPY ./ /app/
|
||||
|
||||
# Change directory to backend
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies on backend
|
||||
RUN npm install
|
||||
|
||||
# Expose the port the app runs in
|
||||
EXPOSE 17442
|
||||
|
||||
# Run the specified command within the container.
|
||||
CMD [ "node", "app.js" ]
|
||||
7
LICENSE.md
Normal file
7
LICENSE.md
Normal file
@@ -0,0 +1,7 @@
|
||||
Copyright 2018 Isaac Grynsztein
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
115
README.md
115
README.md
@@ -1,28 +1,113 @@
|
||||
# YoutubeDLMaterial
|
||||
# YoutubeDL-Material
|
||||
|
||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.1.2.
|
||||
YoutubeDL-Material is a Material Design frontend for [youtube-dl](https://rg3.github.io/youtube-dl/). It's coded using [Angular 8](https://angular.io/) for the frontend, and [Node.js](https://nodejs.org/) on the backend.
|
||||
|
||||
## Development server
|
||||
Now with [Docker](#Docker) support!
|
||||
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||
## Getting Started
|
||||
|
||||
## Code scaffolding
|
||||
Check out the prerequisites, and go to the installation section. Easy as pie!
|
||||
|
||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|module`.
|
||||
Here's an image of what it'll look like once you're done:
|
||||
|
||||
## Build
|
||||

|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build.
|
||||
With optional file management enabled (default):
|
||||
|
||||
## Running unit tests
|
||||

|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
Dark mode:
|
||||
|
||||
## Running end-to-end tests
|
||||

|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||
Before running the tests make sure you are serving the app via `ng serve`.
|
||||
### Prerequisites
|
||||
|
||||
## Further help
|
||||
NOTE: If you would like to use Docker, you can skip down to the [Docker](#Docker) section for a setup guide.
|
||||
|
||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
|
||||
You need to have a functioning web server for this to work. Also make sure you have these dependencies installed on your system: nodejs and youtube-dl. If you don't, run this command:
|
||||
|
||||
```
|
||||
sudo apt-get install nodejs youtube-dl
|
||||
```
|
||||
|
||||
### Installing
|
||||
|
||||
1. First, download the [latest release](https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest)!
|
||||
|
||||
2. Drag all the files in `youtubedl-material` to an easily accessible directory. Navigate to the `config` folder and edit the `default.json` file. If you're using SSL encryption, look at the `encrypted.json` file for a template.
|
||||
|
||||
NOTE: If you are intending to use a reverse proxy, this next step is not necessary
|
||||
3. Port forward the port listed in `default.json`, which defaults to `17442`.
|
||||
|
||||
4. Once the configuration is done, run `npm install` to install all the backend dependencies. Once that is finished, type `nodejs app.js`. This will run the backend server, which serves the frontend as well. On your browser, navigate to to the server (url with the specified port). Try putting in a youtube link to see if it works. If it does, viola! YoutubeDL-Material is now up and running.
|
||||
|
||||
If you experience problems, know that it's usually caused by a configuration problem. The first thing you should do is check the console. To get there, right click anywhere on the page and click "Inspect element." Then on the menu that pops up, click console. Look at the error there, and try to investigate.
|
||||
|
||||
### Configuration
|
||||
|
||||
NOTE: If you are using YoutubeDL-Material v3.2 or lower, click [here](https://github.com/Tzahi12345/YoutubeDL-Material/blob/b87a9f1e2fd896b8e3b2f12429b7ffb15ea4521b/README.md#configuration) for the old README
|
||||
|
||||
Here is an explanation for the configuration entries. Check out the [default config](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/backend/config/default.json) for more context.
|
||||
|
||||
| Config item | Description | Default |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| url | URL to the server hosting YoutubeDL-Material | "http://example.com" |
|
||||
| port | Desired port for YoutubeDL-Material | "17442" |
|
||||
| use-encryption | true if you intend to use SSL encryption (https) | false |
|
||||
| cert-file-path | Cert file path - required if using encryption | "/etc/letsencrypt/live/example.com/fullchain.pem" |
|
||||
| key-file-path | Private key file path - required if using encryption | "/etc/letsencrypt/live/example.com/privkey.pem" |
|
||||
| path-audio | Path to audio folder for saved mp3s | "audio/" |
|
||||
| path-video | Path to video folder for saved mp4s | "video/" |
|
||||
| title_top | Title shown on the top toolbar | "Youtube Downloader" |
|
||||
| file_manager_enabled | true if you want to use the file manager | true |
|
||||
| allow_quality_select | true if you want to select a videos quality level before downloading | true |
|
||||
| download_only_mode | true if you want files to directly download to the client with no media player | false |
|
||||
| allow_multi_download_mode | true if you want the ability to download multiple videos at the same time | true |
|
||||
| use_youtube_API | true if you want to use the Youtube API which is used for YT searches | false |
|
||||
| youtube_API_key | Youtube API key. Required if use_youtube_API is enabled | "" |
|
||||
| default_theme | Default theme to use. Options are "default" and "dark" | "default" |
|
||||
| allow_theme_change | true if you want the icon in the top toolbar that toggles dark mode | true |
|
||||
| use_default_downloading_agent | true if you want to use youtube-dl's default downloader | true |
|
||||
| custom_downloading_agent | If not using the default downloader, this is the downloader you want to use | "" |
|
||||
| allow_advanced_download | true if you want to use the Advanced download options - NOT FULLY IMPLEMENTED | false |
|
||||
|
||||
## Deployment
|
||||
|
||||
If you'd like to install YoutubeDL-Material, go to the Installation section. If you want to build it yourself and/or develop the repository, then this section is for you.
|
||||
|
||||
To deploy, simply clone the repository, and go into the `youtubedl-material` directory. Type `npm install` and all the dependencies will install. Then type `cd backend` and again type `npm install` to install the dependencies for the backend.
|
||||
|
||||
Once you do that, you're almost up and running. All you need to do is edit the configuration in `youtubedl-material/backend/config`, go back into the `youtubedl-material` directory, and type `ng build --prod`. This will build the app, and put the output files in the `youtubedl-material/dist` folder. Drag those files into the `public` directory in the `backend` folder.
|
||||
|
||||
The frontend is now complete. The backend is much easier. Just go into the `backend` folder, and type `nodejs app.js`.
|
||||
|
||||
Finally, port forward the port specified in the config (defaults to `17442`) and point it to the server's IP address. Make sure the port is also allowed through the server's firewall.
|
||||
|
||||
## Docker
|
||||
|
||||
If you are looking to setup YoutubeDL-Material with Docker, this section is for you. And you're in luck! Docker setup is quite simple.
|
||||
|
||||
1. Run `curl -L https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest/download/docker-compose.yml -o docker-compose.yml` to download the latest release of `docker-compose.yml`, or go to the [releases](https://github.com/Tzahi12345/YoutubeDL-Material/releases/) page to grab the version you'd like.
|
||||
2. Modify the config items in the `environment` section of `docker-compose.yml` to your liking. The default options will work, however, and point to `http://localhost:8998`. You can find an explanation of these configuration items in [Configuration](#Configuration) section.
|
||||
3. Run `docker-compose pull`. This will download the official YoutubeDL-Material docker image.
|
||||
4. Run `docker-compose up` to start it up. If successful, it should say "HTTP(S): Started on port 8998" or something similar.
|
||||
5. Make sure you can connect to the specified URL + port, and if so, you are done!
|
||||
|
||||
## Contributing
|
||||
|
||||
Feel free to submit a pull request! I have no guidelines as of yet, so no need to worry about that.
|
||||
|
||||
## Authors
|
||||
|
||||
* **Isaac Grynsztein** (me!) - *Initial work*
|
||||
|
||||
See also the list of [contributors](https://github.com/your/project/contributors) who participated in this project.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
* youtube-dl
|
||||
* [AllTube](https://github.com/Rudloff/alltube) (for the inspiration)
|
||||
|
||||
185
angular.json
Normal file
185
angular.json
Normal file
@@ -0,0 +1,185 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"youtube-dl-material": {
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"projectType": "application",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"assets": [
|
||||
"src/assets",
|
||||
"src/favicon.ico",
|
||||
"src/backend/audio",
|
||||
"src/backend/video",
|
||||
"src/backend"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "youtube-dl-material:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "youtube-dl-material:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "youtube-dl-material:build"
|
||||
}
|
||||
},
|
||||
"serve-electron": {
|
||||
"builder": "@angular-guru/electron-builder:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "youtube-dl-material:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "youtube-dl-material:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"electron": {
|
||||
"builder": "@angular-guru/electron-builder:build",
|
||||
"options": {
|
||||
"outputPath": "dist",
|
||||
"index": "src/index.html",
|
||||
"main": "main.js",
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"assets": [
|
||||
"src/assets",
|
||||
"src/favicon.ico",
|
||||
"src/backend/audio",
|
||||
"src/backend/video",
|
||||
"src/backend"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"karmaConfig": "./karma.conf.js",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "src/tsconfig.spec.json",
|
||||
"scripts": [],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"assets": [
|
||||
"src/assets",
|
||||
"src/favicon.ico",
|
||||
"src/backend/audio",
|
||||
"src/backend/video",
|
||||
"src/backend"
|
||||
]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"src/tsconfig.app.json",
|
||||
"src/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"youtube-dl-material-e2e": {
|
||||
"root": "e2e",
|
||||
"sourceRoot": "e2e",
|
||||
"projectType": "application",
|
||||
"architect": {
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "./protractor.conf.js",
|
||||
"devServerTarget": "youtube-dl-material:serve"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"e2e/tsconfig.e2e.json"
|
||||
],
|
||||
"exclude": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "youtube-dl-material",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"prefix": "app",
|
||||
"styleext": "scss"
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"prefix": "app"
|
||||
}
|
||||
}
|
||||
}
|
||||
1059
backend/app.js
1059
backend/app.js
File diff suppressed because it is too large
Load Diff
BIN
backend/aria2c.exe
Normal file
BIN
backend/aria2c.exe
Normal file
Binary file not shown.
113
backend/config.js
Normal file
113
backend/config.js
Normal file
@@ -0,0 +1,113 @@
|
||||
const fs = require('fs');
|
||||
|
||||
let CONFIG_ITEMS = require('./consts.js')['CONFIG_ITEMS'];
|
||||
|
||||
let configPath = 'config/default.json';
|
||||
|
||||
// https://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key
|
||||
Object.byString = function(o, s) {
|
||||
s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
|
||||
s = s.replace(/^\./, ''); // strip a leading dot
|
||||
var a = s.split('.');
|
||||
for (var i = 0, n = a.length; i < n; ++i) {
|
||||
var k = a[i];
|
||||
if (k in o) {
|
||||
o = o[k];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
function getParentPath(path) {
|
||||
let elements = path.split('.');
|
||||
elements.splice(elements.length - 1, 1);
|
||||
return elements.join('.');
|
||||
}
|
||||
|
||||
function getElementNameInConfig(path) {
|
||||
let elements = path.split('.');
|
||||
return elements[elements.length - 1];
|
||||
}
|
||||
|
||||
/*
|
||||
* Gets config file and returns as a json
|
||||
*/
|
||||
function getConfigFile() {
|
||||
let raw_data = fs.readFileSync(configPath);
|
||||
try {
|
||||
let parsed_data = JSON.parse(raw_data);
|
||||
return parsed_data;
|
||||
} catch(e) {
|
||||
console.log('ERROR: Failed to get config file');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function setConfigFile(config) {
|
||||
try {
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
||||
return true;
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getConfigItem(key) {
|
||||
let config_json = getConfigFile();
|
||||
if (!CONFIG_ITEMS[key]) console.log('cannot find config with key ' + key);
|
||||
let path = CONFIG_ITEMS[key]['path'];
|
||||
return Object.byString(config_json, path);
|
||||
};
|
||||
|
||||
function setConfigItem(key, value) {
|
||||
let success = false;
|
||||
let config_json = getConfigFile();
|
||||
let path = CONFIG_ITEMS[key]['path'];
|
||||
let parent_path = getParentPath(path);
|
||||
let element_name = getElementNameInConfig(path);
|
||||
|
||||
let parent_object = Object.byString(config_json, parent_path);
|
||||
if (value === 'false' || value === 'true') {
|
||||
parent_object[element_name] = (value === 'true');
|
||||
} else {
|
||||
parent_object[element_name] = value;
|
||||
}
|
||||
|
||||
success = setConfigFile(config_json);
|
||||
|
||||
return success;
|
||||
};
|
||||
|
||||
function setConfigItems(items) {
|
||||
let success = false;
|
||||
let config_json = getConfigFile();
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
let key = items[i].key;
|
||||
let value = items[i].value;
|
||||
|
||||
// if boolean strings, set to booleans again
|
||||
if (value === 'false' || value === 'true') {
|
||||
value = (value === 'true');
|
||||
}
|
||||
|
||||
let item_path = CONFIG_ITEMS[key]['path'];
|
||||
let item_parent_path = getParentPath(item_path);
|
||||
let item_element_name = getElementNameInConfig(item_path);
|
||||
|
||||
let item_parent_object = Object.byString(config_json, item_parent_path);
|
||||
item_parent_object[item_element_name] = value;
|
||||
}
|
||||
|
||||
success = setConfigFile(config_json);
|
||||
return success;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getConfigItem: getConfigItem,
|
||||
setConfigItem: setConfigItem,
|
||||
setConfigItems: setConfigItems,
|
||||
getConfigFile: getConfigFile,
|
||||
CONFIG_ITEMS: CONFIG_ITEMS
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"YoutubeDL-Material": {
|
||||
"Host": {
|
||||
"frontend-url": "http://localhost:4200",
|
||||
"backend-url": "youtubedl.grynsztein.com",
|
||||
"backend-port": "8088"
|
||||
},
|
||||
"Encryption": {
|
||||
"use-encryption": false,
|
||||
"cert-file-path": "fullchain.pem",
|
||||
"key-file-path": "privkey.pem"
|
||||
},
|
||||
"Downloader": {
|
||||
"path-base": "http://localhost:8088/",
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,37 @@
|
||||
{
|
||||
"YoutubeDLMaterial": {
|
||||
"Host": {
|
||||
"frontendurl": "http://localhost:4200",
|
||||
"backendurl": "https://youtubedl.grynsztein.com/"
|
||||
"url": "http://example.com",
|
||||
"port": "17442"
|
||||
},
|
||||
"Encryption": {
|
||||
"use-encryption": false,
|
||||
"cert-file-path": "cert.pem",
|
||||
"key-file-path": "privkey.pem"
|
||||
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
|
||||
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
|
||||
},
|
||||
"Downloader": {
|
||||
"path-base": "http://localhost:8088/",
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/"
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "Youtube Downloader"
|
||||
"title_top": "Youtube Downloader",
|
||||
"file_manager_enabled": true,
|
||||
"allow_quality_select": true,
|
||||
"download_only_mode": false,
|
||||
"allow_multi_download_mode": true
|
||||
},
|
||||
"API": {
|
||||
"use_youtube_API": false,
|
||||
"youtube_API_key": ""
|
||||
},
|
||||
"Themes": {
|
||||
"default_theme": "default",
|
||||
"allow_theme_change": true
|
||||
},
|
||||
"Advanced": {
|
||||
"use_default_downloading_agent": true,
|
||||
"custom_downloading_agent": "",
|
||||
"allow_advanced_download": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
37
backend/config/encrypted.json
Normal file
37
backend/config/encrypted.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"YoutubeDLMaterial": {
|
||||
"Host": {
|
||||
"url": "https://example.com",
|
||||
"port": "17442"
|
||||
},
|
||||
"Encryption": {
|
||||
"use-encryption": true,
|
||||
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
|
||||
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
|
||||
},
|
||||
"Downloader": {
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/"
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "Youtube Downloader",
|
||||
"file_manager_enabled": true,
|
||||
"allow_quality_select": true,
|
||||
"download_only_mode": false,
|
||||
"allow_multi_download_mode": true
|
||||
},
|
||||
"API": {
|
||||
"use_youtube_API": false,
|
||||
"youtube_API_key": ""
|
||||
},
|
||||
"Themes": {
|
||||
"default_theme": "default",
|
||||
"allow_theme_change": true
|
||||
},
|
||||
"Advanced": {
|
||||
"use_default_downloading_agent": true,
|
||||
"custom_downloading_agent": "",
|
||||
"allow_advanced_download": false
|
||||
}
|
||||
}
|
||||
}
|
||||
96
backend/consts.js
Normal file
96
backend/consts.js
Normal file
@@ -0,0 +1,96 @@
|
||||
var config = require('config');
|
||||
|
||||
let CONFIG_ITEMS = {
|
||||
// Host
|
||||
'ytdl_url': {
|
||||
'key': 'ytdl_url',
|
||||
'path': 'YoutubeDLMaterial.Host.url'
|
||||
},
|
||||
'ytdl_port': {
|
||||
'key': 'ytdl_port',
|
||||
'path': 'YoutubeDLMaterial.Host.port'
|
||||
},
|
||||
|
||||
// Encryption
|
||||
'ytdl_use_encryption': {
|
||||
'key': 'ytdl_use_encryption',
|
||||
'path': 'YoutubeDLMaterial.Encryption.use-encryption'
|
||||
},
|
||||
'ytdl_cert_file_path': {
|
||||
'key': 'ytdl_cert_file_path',
|
||||
'path': 'YoutubeDLMaterial.Encryption.cert-file-path'
|
||||
},
|
||||
'ytdl_key_file_path': {
|
||||
'key': 'ytdl_key_file_path',
|
||||
'path': 'YoutubeDLMaterial.Encryption.key-file-path'
|
||||
},
|
||||
|
||||
// Downloader
|
||||
'ytdl_audio_folder_path': {
|
||||
'key': 'ytdl_audio_folder_path',
|
||||
'path': 'YoutubeDLMaterial.Downloader.path-audio'
|
||||
},
|
||||
'ytdl_video_folder_path': {
|
||||
'key': 'ytdl_video_folder_path',
|
||||
'path': 'YoutubeDLMaterial.Downloader.path-video'
|
||||
},
|
||||
|
||||
// Extra
|
||||
'ytdl_title_top': {
|
||||
'key': 'ytdl_title_top',
|
||||
'path': 'YoutubeDLMaterial.Extra.title_top'
|
||||
},
|
||||
'ytdl_file_manager_enabled': {
|
||||
'key': 'ytdl_file_manager_enabled',
|
||||
'path': 'YoutubeDLMaterial.Extra.file_manager_enabled'
|
||||
},
|
||||
'ytdl_allow_quality_select': {
|
||||
'key': 'ytdl_allow_quality_select',
|
||||
'path': 'YoutubeDLMaterial.Extra.allow_quality_select'
|
||||
},
|
||||
'ytdl_download_only_mode': {
|
||||
'key': 'ytdl_download_only_mode',
|
||||
'path': 'YoutubeDLMaterial.Extra.download_only_mode'
|
||||
},
|
||||
'ytdl_allow_multi_download_mode': {
|
||||
'key': 'ytdl_allow_multi_download_mode',
|
||||
'path': 'YoutubeDLMaterial.Extra.allow_multi_download_mode'
|
||||
},
|
||||
|
||||
|
||||
// API
|
||||
'ytdl_use_youtube_api': {
|
||||
'key': 'ytdl_use_youtube_api',
|
||||
'path': 'YoutubeDLMaterial.API.use_youtube_API'
|
||||
},
|
||||
'ytdl_youtube_api_key': {
|
||||
'key': 'ytdl_youtube_api_key',
|
||||
'path': 'YoutubeDLMaterial.API.youtube_API_key'
|
||||
},
|
||||
|
||||
// Themes
|
||||
'ytdl_default_theme': {
|
||||
'key': 'ytdl_default_theme',
|
||||
'path': 'YoutubeDLMaterial.Themes.default_theme'
|
||||
},
|
||||
'ytdl_allow_theme_change': {
|
||||
'key': 'ytdl_allow_theme_change',
|
||||
'path': 'YoutubeDLMaterial.Themes.allow_theme_change'
|
||||
},
|
||||
|
||||
// Advanced
|
||||
'ytdl_use_default_downloading_agent': {
|
||||
'key': 'ytdl_use_default_downloading_agent',
|
||||
'path': 'YoutubeDLMaterial.Advanced.use_default_downloading_agent'
|
||||
},
|
||||
'ytdl_custom_downloading_agent': {
|
||||
'key': 'ytdl_custom_downloading_agent',
|
||||
'path': 'YoutubeDLMaterial.Advanced.custom_downloading_agent'
|
||||
},
|
||||
'ytdl_allow_advanced_download': {
|
||||
'key': 'ytdl_allow_advanced_download',
|
||||
'path': 'YoutubeDLMaterial.Advanced.allow_advanced_download'
|
||||
},
|
||||
};
|
||||
|
||||
module.exports.CONFIG_ITEMS = CONFIG_ITEMS;
|
||||
1345
backend/package-lock.json
generated
Normal file
1345
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,23 +1,31 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "1.0.0",
|
||||
"description": "backend for hda",
|
||||
"description": "backend for YoutubeDL-Material",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node app.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Tzahi12345/hda-backend.git"
|
||||
"url": ""
|
||||
},
|
||||
"author": "Isaac Grynsztein",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Tzahi12345/hda-backend/issues"
|
||||
"url": ""
|
||||
},
|
||||
"homepage": "https://github.com/Tzahi12345/hda-backend#readme",
|
||||
"homepage": "",
|
||||
"dependencies": {
|
||||
"archiver": "^3.1.1",
|
||||
"async": "^3.1.0",
|
||||
"compression": "^1.7.4",
|
||||
"config": "^3.2.3",
|
||||
"exe": "^1.0.2",
|
||||
"youtube-dl": "1.11.1"
|
||||
"express": "^4.17.1",
|
||||
"lowdb": "^1.0.0",
|
||||
"shortid": "^2.2.15",
|
||||
"youtube-dl": "^3.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
12
browserslist
Normal file
12
browserslist
Normal file
@@ -0,0 +1,12 @@
|
||||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
> 0.5%
|
||||
last 2 versions
|
||||
Firefox ESR
|
||||
not dead
|
||||
not IE 9-11 # For IE 9-11 support, remove 'not'.
|
||||
BIN
chrome-extension.crx
Normal file
BIN
chrome-extension.crx
Normal file
Binary file not shown.
28
chrome-extension.pem
Normal file
28
chrome-extension.pem
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDMX9Wk5SM5cIfY
|
||||
6ReKX3ybY1rsbNbOzG8ceN7yyeXB0mor8pVsX1MOna2HewOyBuaaYNJRO4tJBxic
|
||||
7a8zQErfgHL/i/QrVvVCpfJ7xKvq6zij5NYoqd/FBUwawqjeH5/voIcAp9z5Vmsr
|
||||
kL0sxJUKy6b4IWNp3noU7Nvq2RwxnXQbKDhz8FrX6oQAnDC6gsG5a2OSPsaE4oqw
|
||||
6nmonORJypmpP5hqyHY8ffXBT2lAxjHT7OnYbaCBe2TQP8+rH6rDBOhjVNtUJ089
|
||||
ocTQL6LtQEPkcF4yKJmtcOwHl8OPGZs5l9i8xb4j9RuSPkm2lbzZX8sOsdGGoqJZ
|
||||
q68nYhsHAgMBAAECggEAXmtKEzfPObq88B/kAcgSk+FngMHZzcmR7bgD3GwdSxnQ
|
||||
dkRI9zvk7eQ35tcUwntAr4Lat6/ILjFqlBmVLxrdXHuF5Xz9jcZLYgKzz61xdYM9
|
||||
dC6FKF0u5eGIIvbauGAo7jaeGFX1F3Zu5b4lP9kEOGwU1B7sxF0FzsQM5+dtCJgv
|
||||
We/hWQeF+9gtoVnkCSS/Mq2p0UomXXHW0Bz4+HuHlTR9aiYbviYnotABiLUhZyzt
|
||||
v5yUaktb9qniBfdLpRlq8cp06xYlTEA9gJpa4Pnok8OWUsbAiW6EiXUSaZ/cchVa
|
||||
AnO8WWYvVOnnt6WHI3+QdFTnqVjE5TBX4N/7bVhHGQKBgQD0dtbFqp7vZK/jVYvE
|
||||
z0WPdySOg2ZDmoSfk5ZlR1+Y9zWToHv0qu8zqoOjL8Ubxrh9fGlOow+cCVdkEuaa
|
||||
jWC2AWetuRvW0Z5A3XMXr0/N/1fgOkTqtp3WNrUPjVJahEg3lN+90opgFoT8swSi
|
||||
s1oxW0oLcVIlrjhGBXAPCfsAuQKBgQDWBLRhHsRAvGcK5wGuVnxVApTIyBOermsW
|
||||
3bJt+7+UI+4sYrBAwkWdQG93IG0cQtn48TEPBgmR2fjRF5IFT9M4/u+QOeeByT7I
|
||||
we7nVtHgSY5ByC9N0mjWbcmSg8fktz/LonjldNC4kWdOFb75fxGf8kOGS5rUaMA4
|
||||
zHucfB6ZvwKBgQCPHJrysMXGY21MaqIeHzEboaX3ABl37hdBzAa5V6UxSVdGCydF
|
||||
vmO2HVZey/JaJmWOoKyNaowSzq0oWqBBTg6VvhDR9JHFmoVId9uOvAS+FYN+Mt5x
|
||||
gWK5KuGoLxVNBC+6yh6JY526TrSfsrU+Aj0Es+qO9FIg2PL8muZVB4S3kQKBgH/5
|
||||
CDMaxpc/EQ5/2413wZjDllwI51J3USm3Hz6Mzp2ybnSz/lh60k2Zfg1polTH1Lb6
|
||||
4i7tmUNRZ2sAARyUAuWN64n+VeRRhe1dqZFDZPQMh7fmEAMk0fOGaoXlrt2ghdEq
|
||||
Mchi9Xun1nHmpu9hgBR4NNBU3RwuFuLfwvprbZDZAoGAWa62QJChE86xQGP1MrL2
|
||||
SbIzw3cfeP5xdQ3MKldJiy5IkbMR7Z13WZ7FwvPTy0g/onLHD1rqlm1kUMsGRHpD
|
||||
5vH06PNpKXQ6x8BYaRGtE6P39jLycO/X+WK/lYTrWo1bR+mGCebDh4B5XrwT3gI6
|
||||
x4Gvz134pZCTyQCf5JCwbQs=
|
||||
-----END PRIVATE KEY-----
|
||||
20
chrome-extension/background.js
Normal file
20
chrome-extension/background.js
Normal file
@@ -0,0 +1,20 @@
|
||||
// background.js
|
||||
|
||||
// Called when the user clicks on the browser action.
|
||||
chrome.browserAction.onClicked.addListener(function(tab) {
|
||||
// get the frontend_url
|
||||
chrome.storage.sync.get({
|
||||
frontend_url: 'http://localhost',
|
||||
audio_only: false
|
||||
}, function(items) {
|
||||
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
|
||||
var activeTab = tabs[0];
|
||||
var url = activeTab.url;
|
||||
if (url.includes('youtube.com')) {
|
||||
var new_url = items.frontend_url + '/#/home;url=' + encodeURIComponent(url) + ';audioOnly=' + items.audio_only;
|
||||
chrome.tabs.create({ url: new_url });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
BIN
chrome-extension/favicon.png
Normal file
BIN
chrome-extension/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
20
chrome-extension/manifest.json
Normal file
20
chrome-extension/manifest.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "YoutubeDL-Material",
|
||||
"version": "0.1",
|
||||
"description": "The official chrome extension of YoutubeDL-Material, an open-source and self-hosted YouTube downloader.",
|
||||
"background": {
|
||||
"scripts": ["background.js"]
|
||||
},
|
||||
"browser_action": {
|
||||
"default_icon": "favicon.png"
|
||||
},
|
||||
"permissions": [
|
||||
"tabs",
|
||||
"storage"
|
||||
],
|
||||
"options_ui": {
|
||||
"page": "options.html",
|
||||
"open_in_tab": false
|
||||
}
|
||||
}
|
||||
29
chrome-extension/options.html
Normal file
29
chrome-extension/options.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>YoutubeDL-Material Extension Options</title></head>
|
||||
<body>
|
||||
|
||||
<h2>Settings</h2>
|
||||
|
||||
<div>
|
||||
<h4>Frontend URL</h4>
|
||||
<input placeholder="Frontend URL" type="text" id="frontend_url">
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
<input type="checkbox" id="audio_only">
|
||||
Audio only
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div id="status"></div>
|
||||
<button id="save">Save</button>
|
||||
|
||||
<script src="options.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
31
chrome-extension/options.js
Normal file
31
chrome-extension/options.js
Normal file
@@ -0,0 +1,31 @@
|
||||
// Saves options to chrome.storage
|
||||
function save_options() {
|
||||
var frontend_url = document.getElementById('frontend_url').value;
|
||||
var audio_only = document.getElementById('audio_only').checked;
|
||||
chrome.storage.sync.set({
|
||||
frontend_url: frontend_url,
|
||||
audio_only: audio_only
|
||||
}, function() {
|
||||
// Update status to let user know options were saved.
|
||||
var status = document.getElementById('status');
|
||||
status.textContent = 'Options saved.';
|
||||
setTimeout(function() {
|
||||
status.textContent = '';
|
||||
}, 750);
|
||||
});
|
||||
}
|
||||
|
||||
// Restores select box and checkbox state using the preferences
|
||||
// stored in chrome.storage.
|
||||
function restore_options() {
|
||||
chrome.storage.sync.get({
|
||||
frontend_url: 'http://localhost',
|
||||
audio_only: false
|
||||
}, function(items) {
|
||||
document.getElementById('frontend_url').value = items.frontend_url;
|
||||
document.getElementById('audio_only').checked = items.audio_only;
|
||||
});
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', restore_options);
|
||||
document.getElementById('save').addEventListener('click',
|
||||
save_options);
|
||||
33
docker-compose.yml
Normal file
33
docker-compose.yml
Normal file
@@ -0,0 +1,33 @@
|
||||
|
||||
version: "2"
|
||||
services:
|
||||
ytdl_material:
|
||||
build: .
|
||||
environment:
|
||||
# config items
|
||||
ytdl_url: http://localhost:8998
|
||||
ytdl_port: '17442'
|
||||
ytdl_use_encryption: 'false'
|
||||
ytdl_cert_file_path: /etc/letsencrypt/live/example.com/fullchain.pem
|
||||
ytdl_key_file_path: /etc/letsencrypt/live/example.com/privkey.pem
|
||||
ytdl_audio_folder_path: audio/
|
||||
ytdl_video_folder_path: video/
|
||||
ytdl_title_top: Youtube Downloader
|
||||
ytdl_file_manager_enabled: 'true'
|
||||
ytdl_allow_quality_select: 'true'
|
||||
ytdl_download_only_mode: 'false'
|
||||
ytdl_allow_multi_download_mode: 'true'
|
||||
ytdl_use_youtube_api: 'false'
|
||||
ytdl_youtube_api_key: 'false'
|
||||
ytdl_default_theme: default
|
||||
ytdl_allow_theme_change: 'true'
|
||||
ytdl_use_default_downloading_agent: 'true'
|
||||
ytdl_custom_downloading_agent: 'false'
|
||||
ytdl_allow_advanced_download: 'false'
|
||||
# do not touch this
|
||||
write_ytdl_config: 'true'
|
||||
ALLOW_CONFIG_MUTATIONS: 'true'
|
||||
restart: always
|
||||
ports:
|
||||
- "8998:17442"
|
||||
image: tzahi12345/youtubedl-material:3.3
|
||||
39
docker_wrapper.sh
Normal file
39
docker_wrapper.sh
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd backend
|
||||
|
||||
# Start the first process
|
||||
node app.js &
|
||||
status=$?
|
||||
if [ $status -ne 0 ]; then
|
||||
echo "Failed to start my_first_process: $status"
|
||||
exit $status
|
||||
fi
|
||||
|
||||
# Start the second process
|
||||
apachectl -DFOREGROUND
|
||||
status=$?
|
||||
if [ $status -ne 0 ]; then
|
||||
echo "Failed to start my_second_process: $status"
|
||||
exit $status
|
||||
fi
|
||||
|
||||
# Naive check runs checks once a minute to see if either of the processes exited.
|
||||
# This illustrates part of the heavy lifting you need to do if you want to run
|
||||
# more than one service in a container. The container will exit with an error
|
||||
# if it detects that either of the processes has exited.
|
||||
# Otherwise it will loop forever, waking up every 60 seconds
|
||||
|
||||
while /bin/true; do
|
||||
ps aux |grep node\ app.js # |grep -q -v grep
|
||||
PROCESS_1_STATUS=$?
|
||||
ps aux |grep apache2 # |grep -q -v grep
|
||||
PROCESS_2_STATUS=$?
|
||||
# If the greps above find anything, they will exit with 0 status
|
||||
# If they are not both 0, then something is wrong
|
||||
if [ $PROCESS_1_STATUS -ne 0 -o $PROCESS_2_STATUS -ne 0 ]; then
|
||||
echo "One of the processes has already exited."
|
||||
exit -1
|
||||
fi
|
||||
sleep 60
|
||||
done
|
||||
0
installer.py
Normal file
0
installer.py
Normal file
@@ -4,24 +4,22 @@
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular/cli'],
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage-istanbul-reporter'),
|
||||
require('@angular/cli/plugins/karma')
|
||||
require('@angular-devkit/build-angular/plugins/karma')
|
||||
],
|
||||
client:{
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
coverageIstanbulReporter: {
|
||||
reports: [ 'html', 'lcovonly' ],
|
||||
dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ],
|
||||
fixWebpackSourcePaths: true
|
||||
},
|
||||
angularCli: {
|
||||
environment: 'dev'
|
||||
},
|
||||
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
|
||||
41
main.js
Normal file
41
main.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
const path = require('path');
|
||||
const url = require('url');
|
||||
|
||||
let win;
|
||||
|
||||
function createWindow() {
|
||||
win = new BrowserWindow({ width: 800, height: 600 });
|
||||
|
||||
// load the dist folder from Angular
|
||||
win.loadURL(
|
||||
url.format({
|
||||
pathname: path.join(__dirname, `/dist/index.html`),
|
||||
protocol: 'file:',
|
||||
slashes: true
|
||||
})
|
||||
);
|
||||
|
||||
// The following is optional and will open the DevTools:
|
||||
// win.webContents.openDevTools()
|
||||
|
||||
win.on('closed', () => {
|
||||
win = null;
|
||||
});
|
||||
}
|
||||
|
||||
app.on('ready', createWindow);
|
||||
|
||||
// on macOS, closing the window doesn't quit the app
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// initialize the app's main window
|
||||
app.on('activate', () => {
|
||||
if (win === null) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
12768
package-lock.json
generated
Normal file
12768
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
58
package.json
58
package.json
@@ -8,44 +8,62 @@
|
||||
"build": "ng build",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e"
|
||||
"e2e": "ng e2e",
|
||||
"electron": "ng build --base-href ./ && electron ."
|
||||
},
|
||||
"engines": {
|
||||
"node": "12.3.1",
|
||||
"npm": "6.10.3"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^5.2.0",
|
||||
"@angular/cdk": "^5.0.4",
|
||||
"@angular/common": "^5.2.0",
|
||||
"@angular/compiler": "^5.2.0",
|
||||
"@angular/core": "^5.2.0",
|
||||
"@angular/forms": "^5.0.0",
|
||||
"@angular/http": "^5.0.0",
|
||||
"@angular/material": "^5.0.4",
|
||||
"@angular/platform-browser": "^5.0.0",
|
||||
"@angular/platform-browser-dynamic": "^5.0.0",
|
||||
"@angular/router": "^5.0.0",
|
||||
"@angular-devkit/core": "^8.3.12",
|
||||
"@angular/animations": "^8.2.11",
|
||||
"@angular/cdk": "^8.2.3",
|
||||
"@angular/common": "^8.2.11",
|
||||
"@angular/compiler": "^8.2.11",
|
||||
"@angular/core": "^8.2.11",
|
||||
"@angular/forms": "^8.2.11",
|
||||
"@angular/http": "^7.2.15",
|
||||
"@angular/material": "^8.2.3",
|
||||
"@angular/platform-browser": "^8.2.11",
|
||||
"@angular/platform-browser-dynamic": "^8.2.11",
|
||||
"@angular/router": "^8.2.11",
|
||||
"core-js": "^2.4.1",
|
||||
"file-saver": "^2.0.2",
|
||||
"ng-lazyload-image": "^7.0.1",
|
||||
"ng4-configure": "^0.1.7",
|
||||
"rxjs": "^5.5.3",
|
||||
"zone.js": "^0.8.4"
|
||||
"ngx-content-loading": "^0.1.3",
|
||||
"rxjs": "^6.5.3",
|
||||
"rxjs-compat": "^6.0.0-rc.0",
|
||||
"tslib": "^1.10.0",
|
||||
"videogular2": "^7.0.1",
|
||||
"web-animations-js": "^2.3.2",
|
||||
"zone.js": "~0.9.1",
|
||||
"typescript": "~3.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/cli": "1.5.5",
|
||||
"@angular/compiler-cli": "^5.0.0",
|
||||
"@angular/language-service": "^4.0.0",
|
||||
"@angular-devkit/build-angular": "^0.803.24",
|
||||
"@angular/cli": "^8.3.12",
|
||||
"@angular/compiler-cli": "^8.2.11",
|
||||
"@angular/language-service": "^8.2.11",
|
||||
"@types/core-js": "^2.5.2",
|
||||
"@types/file-saver": "^2.0.1",
|
||||
"@types/jasmine": "2.5.45",
|
||||
"@types/node": "~6.0.60",
|
||||
"codelyzer": "~3.0.1",
|
||||
"codelyzer": "^5.0.1",
|
||||
"electron": "^8.0.1",
|
||||
"jasmine-core": "~2.6.2",
|
||||
"jasmine-spec-reporter": "~4.1.0",
|
||||
"karma": "~1.7.0",
|
||||
"karma-chrome-launcher": "~2.1.1",
|
||||
"karma-cli": "~1.0.1",
|
||||
"karma-coverage-istanbul-reporter": "^1.2.1",
|
||||
"karma-jasmine": "~1.1.0",
|
||||
"karma-jasmine-html-reporter": "^0.2.2",
|
||||
"karma-coverage-istanbul-reporter": "^1.2.1",
|
||||
"protractor": "~5.1.2",
|
||||
"ts-node": "~3.0.4",
|
||||
"tslint": "~5.3.2",
|
||||
"typescript": "~2.5.3"
|
||||
"typescript": "~3.5.3"
|
||||
}
|
||||
}
|
||||
|
||||
15
src/_palette.scss
Normal file
15
src/_palette.scss
Normal file
@@ -0,0 +1,15 @@
|
||||
/* Coolors Exported Palette - coolors.co/e8aeb7-b8e1ff-a9fff7-94fbab-82aba1 */
|
||||
|
||||
/* HSL */
|
||||
$color1: hsla(351%, 56%, 80%, 1);
|
||||
$softblue: hsla(205%, 100%, 86%, 1);
|
||||
$color3: hsla(174%, 100%, 83%, 1);
|
||||
$color4: hsla(133%, 93%, 78%, 1);
|
||||
$color5: hsla(165%, 20%, 59%, 1);
|
||||
|
||||
/* RGB */
|
||||
$color1: rgba(232, 174, 183, 1);
|
||||
$softblue: rgba(184, 225, 255, 1);
|
||||
$color3: rgba(169, 255, 247, 1);
|
||||
$color4: rgba(148, 251, 171, 1);
|
||||
$color5: rgba(130, 171, 161, 1);
|
||||
15
src/app/app-routing.module.ts
Normal file
15
src/app/app-routing.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { MainComponent } from './main/main.component';
|
||||
import { PlayerComponent } from './player/player.component';
|
||||
const routes: Routes = [
|
||||
{ path: 'home', component: MainComponent },
|
||||
{ path: 'player', component: PlayerComponent},
|
||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes, { useHash: true })],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
@@ -1,41 +1,13 @@
|
||||
.demo-card {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.demo-basic {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.demo-basic .mat-card-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
mat-toolbar.top {
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/*::ng-deep .mat-form-field-placeholder{
|
||||
|
||||
transform: scale(.75) translateY(20px) !important;
|
||||
}*/
|
||||
|
||||
.big {
|
||||
width: 60%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.centered {
|
||||
margin: 0 auto;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.example-full-width {
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
mat-form-field.mat-form-field {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-basis: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
@@ -1,45 +1,17 @@
|
||||
<mat-toolbar color="primary" class="top">
|
||||
<table width="100%" height="100%">
|
||||
<td class="topbar" style="text-align: left; left:0px; font-size: 15px">
|
||||
</td>
|
||||
<td class="topbar" style="text-align: center">
|
||||
<div style="margin-top: 14px">{{topBarTitle}}</div>
|
||||
</td>
|
||||
<td class="topbar" style="text-align: right">
|
||||
|
||||
</td>
|
||||
</table>
|
||||
</mat-toolbar>
|
||||
|
||||
<br/>
|
||||
|
||||
<mat-card id="card" class="big demo-basic">
|
||||
<mat-card-title>
|
||||
<mat-toolbar color="primary">Youtube Downloader</mat-toolbar>
|
||||
</mat-card-title>
|
||||
<div style="width: 100%; height: 100%; padding-left: 24px; padding-right: 24px; position: relative">
|
||||
<mat-card-content>
|
||||
<form class="example-form">
|
||||
<mat-form-field class="example-full-width">
|
||||
<input matInput [(ngModel)]="url" placeholder="URL" type="url" name="url" [formControl]="urlForm" required>
|
||||
<mat-error *ngIf="urlError || urlForm.invalid">Please enter a valid URL!</mat-error>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
<br/>
|
||||
<mat-checkbox [(ngModel)]="audioOnly" style="float: left; margin-top: -12px">Only Audio</mat-checkbox>
|
||||
<button style="float: right; margin-top: -16px" (click)="downloadClicked()" [disabled]="downloadingfile" type="submit" mat-raised-button color="primary">Download</button>
|
||||
</mat-card-content>
|
||||
</div>
|
||||
</mat-card>
|
||||
<br/>
|
||||
<div class="centered big" id="bar_div" *ngIf="downloadingfile;else nofile">
|
||||
<div *ngIf="determinateProgress;else indeterminateprogress">
|
||||
<mat-progress-bar mode="determinate" value="{{percentDownloaded}}"></mat-progress-bar>
|
||||
<div [style.background]="postsService.theme ? postsService.theme.background_color : null" style="width: 100%; min-height: 100%;">
|
||||
<mat-toolbar color="primary" class="top">
|
||||
<div class="flex-row" width="100%" height="100%">
|
||||
<div class="flex-column" style="text-align: left; margin-top: 1px;">
|
||||
<button (click)="goBack()" *ngIf="router.url.split(';')[0] === '/player'" mat-icon-button><mat-icon>arrow_back</mat-icon></button>
|
||||
</div>
|
||||
<div class="flex-column" style="text-align: center; margin-top: 5px;">
|
||||
<div>{{topBarTitle}}</div>
|
||||
</div>
|
||||
<div class="flex-column" style="text-align: right; align-items: flex-end;">
|
||||
<button *ngIf="allowThemeChange" mat-icon-button (click)="flipTheme()"><mat-icon>{{(postsService.theme.key === 'default') ? 'brightness_5' : 'brightness_2'}}</mat-icon></button>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #indeterminateprogress>
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
</ng-template>
|
||||
</div>
|
||||
<ng-template #nofile>
|
||||
</mat-toolbar>
|
||||
|
||||
</ng-template>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
@@ -1,176 +1,121 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, ElementRef, ViewChild, HostBinding } from '@angular/core';
|
||||
import {PostsService} from './posts.services';
|
||||
import {FileCardComponent} from './file-card/file-card.component';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import {FormControl, Validators} from '@angular/forms';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {MatSnackBar} from '@angular/material';
|
||||
import { saveAs } from 'file-saver';
|
||||
import 'rxjs/add/observable/of';
|
||||
import 'rxjs/add/operator/mapTo';
|
||||
import 'rxjs/add/operator/toPromise';
|
||||
import 'rxjs/add/observable/fromEvent'
|
||||
import 'rxjs/add/operator/filter'
|
||||
import 'rxjs/add/operator/debounceTime'
|
||||
import 'rxjs/add/operator/do'
|
||||
import 'rxjs/add/operator/switch'
|
||||
import { YoutubeSearchService, Result } from './youtube-search.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { OverlayContainer } from '@angular/cdk/overlay';
|
||||
import { THEMES_CONFIG } from '../themes';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent {
|
||||
determinateProgress: boolean = false;
|
||||
downloadingfile: boolean = false;
|
||||
audioOnly: boolean;
|
||||
urlError: boolean = false;
|
||||
path: string = '';
|
||||
url: string = '';
|
||||
exists: string = "";
|
||||
topBarTitle: string = "Youtube Downloader";
|
||||
percentDownloaded: number;
|
||||
constructor(private postsService: PostsService, public snackBar: MatSnackBar) {
|
||||
this.audioOnly = true;
|
||||
export class AppComponent implements OnInit {
|
||||
|
||||
|
||||
@HostBinding('class') componentCssClass;
|
||||
THEMES_CONFIG = THEMES_CONFIG;
|
||||
|
||||
this.postsService.loadNavItems().subscribe(result => { // loads settings
|
||||
var backendUrl = result.YoutubeDLMaterial.Host.backendurl;
|
||||
this.topBarTitle = result.YoutubeDLMaterial.Extra.title_top;
|
||||
// config items
|
||||
topBarTitle = 'Youtube Downloader';
|
||||
defaultTheme = null;
|
||||
allowThemeChange = null;
|
||||
|
||||
this.postsService.path = backendUrl;
|
||||
this.postsService.startPath = backendUrl;
|
||||
this.postsService.startPathSSL = backendUrl;
|
||||
},
|
||||
error => {
|
||||
@ViewChild('urlinput', { read: ElementRef, static: false }) urlInput: ElementRef;
|
||||
|
||||
constructor(public postsService: PostsService, public snackBar: MatSnackBar,
|
||||
public router: Router, public overlayContainer: OverlayContainer, private elementRef: ElementRef) {
|
||||
|
||||
// loading config
|
||||
this.postsService.loadNavItems().subscribe(res => { // loads settings
|
||||
const result = !this.postsService.debugMode ? res['config_file'] : res;
|
||||
this.topBarTitle = result['YoutubeDLMaterial']['Extra']['title_top'];
|
||||
const themingExists = result['YoutubeDLMaterial']['Themes'];
|
||||
this.defaultTheme = themingExists ? result['YoutubeDLMaterial']['Themes']['default_theme'] : 'default';
|
||||
this.allowThemeChange = themingExists ? result['YoutubeDLMaterial']['Themes']['allow_theme_change'] : true;
|
||||
|
||||
// sets theme to config default if it doesn't exist
|
||||
if (!localStorage.getItem('theme')) {
|
||||
this.setTheme(themingExists ? this.defaultTheme : 'default');
|
||||
}
|
||||
}, error => {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
urlForm = new FormControl('', [Validators.required]);
|
||||
// theme stuff
|
||||
|
||||
doHandshake(url: string) {
|
||||
this.postsService.startHandshake(url).subscribe(theurl => {
|
||||
this.postsService.path = theurl;
|
||||
this.postsService.handShakeComplete = true;
|
||||
console.log("Handshake complete!");
|
||||
},
|
||||
error => {
|
||||
console.log("Initial handshake failed on http.");
|
||||
this.doHandshakeSSL(url);
|
||||
});
|
||||
setTheme(theme) {
|
||||
// theme is registered, so set it to the stored cookie variable
|
||||
let old_theme = null;
|
||||
if (this.THEMES_CONFIG[theme]) {
|
||||
if (localStorage.getItem('theme')) {
|
||||
old_theme = localStorage.getItem('theme');
|
||||
if (!this.THEMES_CONFIG[old_theme]) {
|
||||
console.log('bad theme found, setting to default');
|
||||
if (this.defaultTheme === null) {
|
||||
// means it hasn't loaded yet
|
||||
console.error('No default theme detected');
|
||||
} else {
|
||||
localStorage.setItem('theme', this.defaultTheme);
|
||||
old_theme = localStorage.getItem('theme'); // updates old_theme
|
||||
}
|
||||
}
|
||||
}
|
||||
localStorage.setItem('theme', theme);
|
||||
this.elementRef.nativeElement.ownerDocument.body.style.backgroundColor = this.THEMES_CONFIG[theme]['background_color'];
|
||||
} else {
|
||||
console.error('Invalid theme: ' + theme);
|
||||
return;
|
||||
}
|
||||
|
||||
this.postsService.setTheme(theme);
|
||||
|
||||
this.onSetTheme(this.THEMES_CONFIG[theme]['css_label'], old_theme ? this.THEMES_CONFIG[old_theme]['css_label'] : old_theme);
|
||||
}
|
||||
|
||||
onSetTheme(theme, old_theme) {
|
||||
if (old_theme) {
|
||||
document.body.classList.remove(old_theme);
|
||||
this.overlayContainer.getContainerElement().classList.remove(old_theme);
|
||||
}
|
||||
this.overlayContainer.getContainerElement().classList.add(theme);
|
||||
this.componentCssClass = theme;
|
||||
}
|
||||
|
||||
doHandshakeSSL(url: string) {
|
||||
this.postsService.startHandshakeSSL(url).subscribe(theurl => {
|
||||
this.postsService.path = theurl;
|
||||
this.postsService.handShakeComplete = true;
|
||||
console.log("Handshake complete!");
|
||||
},
|
||||
error => {
|
||||
console.log("Initial handshake failed on https too! Make sure port 17442 is open.");
|
||||
this.postsService.handShakeComplete = false;
|
||||
});
|
||||
flipTheme() {
|
||||
if (this.postsService.theme.key === 'default') {
|
||||
this.setTheme('dark');
|
||||
} else if (this.postsService.theme.key === 'dark') {
|
||||
this.setTheme('default');
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
downloadHelperMp3(name: string)
|
||||
{
|
||||
this.postsService.getFileStatusMp3(name).subscribe(fileExists => {
|
||||
var exists = fileExists;
|
||||
this.exists = exists[0];
|
||||
if (exists[0] == "failed")
|
||||
{
|
||||
var percent = exists[2];
|
||||
console.log(percent);
|
||||
if (percent > 0.30)
|
||||
{
|
||||
this.determinateProgress = true;
|
||||
this.percentDownloaded = percent*100;
|
||||
}
|
||||
setTimeout(() => this.downloadHelperMp3(name), 500);
|
||||
}
|
||||
else
|
||||
{
|
||||
window.location.href = this.exists;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
downloadHelperMp4(name: string)
|
||||
{
|
||||
this.postsService.getFileStatusMp4(name).subscribe(fileExists => {
|
||||
var exists = fileExists;
|
||||
this.exists = exists[0];
|
||||
if (exists[0] == "failed")
|
||||
{
|
||||
var percent = exists[2];
|
||||
if (percent > 0.30)
|
||||
{
|
||||
this.determinateProgress = true;
|
||||
this.percentDownloaded = percent*100;
|
||||
}
|
||||
setTimeout(() => this.downloadHelperMp4(name), 500);
|
||||
}
|
||||
else
|
||||
{
|
||||
window.location.href = this.exists;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
downloadClicked()
|
||||
{
|
||||
if (this.ValidURL(this.url))
|
||||
{
|
||||
this.urlError = false;
|
||||
this.path = "";
|
||||
|
||||
if (this.audioOnly)
|
||||
{
|
||||
this.downloadingfile = true;
|
||||
this.postsService.makeMP3(this.url).subscribe(posts => {
|
||||
this.path = posts;
|
||||
if (this.path != "-1")
|
||||
{
|
||||
this.downloadHelperMp3(this.path);
|
||||
}
|
||||
},
|
||||
error => { // can't access server
|
||||
this.downloadingfile = false;
|
||||
this.openSnackBar("Download failed!", "OK.");
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
this.downloadingfile = true;
|
||||
this.postsService.makeMP4(this.url).subscribe(posts => {
|
||||
this.path = posts;
|
||||
if (this.path != "-1")
|
||||
{
|
||||
this.downloadHelperMp4(this.path);
|
||||
}
|
||||
},
|
||||
error => { // can't access server
|
||||
this.downloadingfile = false;
|
||||
this.openSnackBar("Download failed!", "OK.");
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.urlError = true;
|
||||
if (localStorage.getItem('theme')) {
|
||||
this.setTheme(localStorage.getItem('theme'));
|
||||
} else {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
ValidURL(str) {
|
||||
var strRegex = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/;
|
||||
var re=new RegExp(strRegex);
|
||||
return re.test(str);
|
||||
}
|
||||
|
||||
openSnackBar(message: string, action: string) {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 2000,
|
||||
});
|
||||
goBack() {
|
||||
this.router.navigate(['/home']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,47 @@
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
import {MatNativeDateModule, MatRadioModule, MatInputModule, MatButtonModule, MatSidenavModule, MatIconModule, MatListModule,
|
||||
MatSnackBarModule, MatCardModule, MatSelectModule, MatToolbarModule, MatCheckboxModule,
|
||||
MatProgressBarModule } from '@angular/material';
|
||||
MatSnackBarModule, MatCardModule, MatSelectModule, MatToolbarModule, MatCheckboxModule, MatGridListModule,
|
||||
MatProgressBarModule, MatExpansionModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatButtonToggleModule,
|
||||
MatDialogModule} from '@angular/material';
|
||||
import {DragDropModule} from '@angular/cdk/drag-drop';
|
||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import { AppComponent } from './app.component';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import { HttpModule } from '@angular/http';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { HttpClientModule, HttpClient } from '@angular/common/http';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import {APP_BASE_HREF} from '@angular/common';
|
||||
import { FileCardComponent } from './file-card/file-card.component';
|
||||
import {RouterModule} from '@angular/router';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { MainComponent } from './main/main.component';
|
||||
import { PlayerComponent } from './player/player.component';
|
||||
import {VgCoreModule} from 'videogular2/compiled/core';
|
||||
import {VgControlsModule} from 'videogular2/compiled/controls';
|
||||
import {VgOverlayPlayModule} from 'videogular2/compiled/overlay-play';
|
||||
import {VgBufferingModule} from 'videogular2/compiled/buffering';
|
||||
import { InputDialogComponent } from './input-dialog/input-dialog.component';
|
||||
import { LazyLoadImageModule, IsVisibleProps } from 'ng-lazyload-image';
|
||||
import { NgxContentLoadingModule } from 'ngx-content-loading';
|
||||
import { audioFilesMouseHovering, videoFilesMouseHovering, audioFilesOpened, videoFilesOpened } from './main/main.component';
|
||||
import { CreatePlaylistComponent } from './create-playlist/create-playlist.component';
|
||||
import { DownloadItemComponent } from './download-item/download-item.component';
|
||||
|
||||
export function isVisible({ event, element, scrollContainer, offset }: IsVisibleProps<any>) {
|
||||
return (element.id === 'video' ? videoFilesMouseHovering || videoFilesOpened : audioFilesMouseHovering || audioFilesOpened);
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
AppComponent,
|
||||
FileCardComponent,
|
||||
MainComponent,
|
||||
PlayerComponent,
|
||||
InputDialogComponent,
|
||||
CreatePlaylistComponent,
|
||||
DownloadItemComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@@ -34,7 +62,25 @@ import {APP_BASE_HREF} from '@angular/common';
|
||||
MatSidenavModule,
|
||||
MatIconModule,
|
||||
MatListModule,
|
||||
MatProgressBarModule
|
||||
MatGridListModule,
|
||||
MatExpansionModule,
|
||||
MatProgressBarModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatButtonToggleModule,
|
||||
MatDialogModule,
|
||||
DragDropModule,
|
||||
VgCoreModule,
|
||||
VgControlsModule,
|
||||
VgOverlayPlayModule,
|
||||
VgBufferingModule,
|
||||
LazyLoadImageModule.forRoot({ isVisible }),
|
||||
NgxContentLoadingModule,
|
||||
RouterModule,
|
||||
AppRoutingModule,
|
||||
],
|
||||
entryComponents: [
|
||||
InputDialogComponent,
|
||||
CreatePlaylistComponent
|
||||
],
|
||||
providers: [PostsService],
|
||||
bootstrap: [AppComponent]
|
||||
|
||||
19
src/app/create-playlist/create-playlist.component.html
Normal file
19
src/app/create-playlist/create-playlist.component.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<h4 mat-dialog-title>Create a playlist</h4>
|
||||
<form>
|
||||
<div>
|
||||
<mat-form-field color="accent">
|
||||
<input [(ngModel)]="name" matInput placeholder="Name" type="text" required aria-required [ngModelOptions]="{standalone: true}">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field color="accent">
|
||||
<mat-label>{{(type === 'audio') ? 'Audio files' : 'Videos'}}</mat-label>
|
||||
<mat-select [formControl]="filesSelect" multiple required aria-required>
|
||||
<mat-option *ngFor="let file of filesToSelectFrom" [value]="file.id">{{file.id}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div *ngIf="create_in_progress" style="float: left"><mat-spinner [diameter]="25"></mat-spinner></div>
|
||||
<button (click)="createPlaylist()" [disabled]="!name || !filesSelect.value || filesSelect.value.length === 0" color="primary" style="float: right" mat-flat-button>Create</button>
|
||||
25
src/app/create-playlist/create-playlist.component.spec.ts
Normal file
25
src/app/create-playlist/create-playlist.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CreatePlaylistComponent } from './create-playlist.component';
|
||||
|
||||
describe('CreatePlaylistComponent', () => {
|
||||
let component: CreatePlaylistComponent;
|
||||
let fixture: ComponentFixture<CreatePlaylistComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ CreatePlaylistComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CreatePlaylistComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
58
src/app/create-playlist/create-playlist.component.ts
Normal file
58
src/app/create-playlist/create-playlist.component.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Component, OnInit, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-playlist',
|
||||
templateUrl: './create-playlist.component.html',
|
||||
styleUrls: ['./create-playlist.component.scss']
|
||||
})
|
||||
export class CreatePlaylistComponent implements OnInit {
|
||||
// really "createPlaylistDialogComponent"
|
||||
|
||||
filesToSelectFrom = null;
|
||||
type = null;
|
||||
filesSelect = new FormControl();
|
||||
name = '';
|
||||
|
||||
create_in_progress = false;
|
||||
|
||||
constructor(@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
private postsService: PostsService,
|
||||
public dialogRef: MatDialogRef<CreatePlaylistComponent>) { }
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
if (this.data) {
|
||||
this.filesToSelectFrom = this.data.filesToSelectFrom;
|
||||
this.type = this.data.type;
|
||||
}
|
||||
}
|
||||
|
||||
createPlaylist() {
|
||||
const thumbnailURL = this.getThumbnailURL();
|
||||
this.create_in_progress = true;
|
||||
this.postsService.createPlaylist(this.name, this.filesSelect.value, this.type, thumbnailURL).subscribe(res => {
|
||||
this.create_in_progress = false;
|
||||
if (res['success']) {
|
||||
this.dialogRef.close(true);
|
||||
} else {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getThumbnailURL() {
|
||||
for (let i = 0; i < this.filesToSelectFrom.length; i++) {
|
||||
const file = this.filesToSelectFrom[i];
|
||||
if (file.id === this.filesSelect.value[0]) {
|
||||
// different services store the thumbnail in different places
|
||||
if (file.thumbnailURL) { return file.thumbnailURL };
|
||||
if (file.thumbnail) { return file.thumbnail };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
16
src/app/download-item/download-item.component.html
Normal file
16
src/app/download-item/download-item.component.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<div>
|
||||
<mat-grid-list [rowHeight]="50" [cols]="24">
|
||||
<mat-grid-tile [colspan]="2">
|
||||
<h5 style="display: inline-block; margin-right: 5px; position: relative; top: 5px;">{{queueNumber}}.</h5>
|
||||
</mat-grid-tile>
|
||||
<mat-grid-tile [colspan]="6">
|
||||
<div style="display: inline-block; text-align: center;">ID: {{url_id}}</div>
|
||||
</mat-grid-tile>
|
||||
<mat-grid-tile [colspan]="13">
|
||||
<mat-progress-bar style="width: 80%" [value]="download.percent_complete" [mode]="(download.percent_complete === 0) ? 'indeterminate' : 'determinate'"></mat-progress-bar>
|
||||
</mat-grid-tile>
|
||||
<mat-grid-tile [colspan]="3">
|
||||
<button (click)="cancelTheDownload()" mat-icon-button color="warn"><mat-icon fontSet="material-icons-outlined">cancel</mat-icon></button>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
</div>
|
||||
0
src/app/download-item/download-item.component.scss
Normal file
0
src/app/download-item/download-item.component.scss
Normal file
25
src/app/download-item/download-item.component.spec.ts
Normal file
25
src/app/download-item/download-item.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DownloadItemComponent } from './download-item.component';
|
||||
|
||||
describe('DownloadItemComponent', () => {
|
||||
let component: DownloadItemComponent;
|
||||
let fixture: ComponentFixture<DownloadItemComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ DownloadItemComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DownloadItemComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
41
src/app/download-item/download-item.component.ts
Normal file
41
src/app/download-item/download-item.component.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Component, OnInit, Output, EventEmitter, Input } from '@angular/core';
|
||||
import { Download } from 'app/main/main.component';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-download-item',
|
||||
templateUrl: './download-item.component.html',
|
||||
styleUrls: ['./download-item.component.scss']
|
||||
})
|
||||
export class DownloadItemComponent implements OnInit {
|
||||
|
||||
@Input() download: Download = {
|
||||
uid: null,
|
||||
type: 'audio',
|
||||
percent_complete: 0,
|
||||
url: 'http://youtube.com/watch?v=17848rufj',
|
||||
downloading: true,
|
||||
is_playlist: false
|
||||
};
|
||||
@Output() cancelDownload = new EventEmitter<Download>();
|
||||
|
||||
@Input() queueNumber = null;
|
||||
|
||||
url_id = null;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
if (this.download && this.download.url && this.download.url.includes('youtube')) {
|
||||
const string_id = (this.download.is_playlist ? '?list=' : '?v=')
|
||||
const index_offset = (this.download.is_playlist ? 6 : 3);
|
||||
const end_index = this.download.url.indexOf(string_id) + index_offset;
|
||||
this.url_id = this.download.url.substring(end_index, this.download.url.length);
|
||||
}
|
||||
}
|
||||
|
||||
cancelTheDownload() {
|
||||
this.cancelDownload.emit(this.download);
|
||||
}
|
||||
|
||||
}
|
||||
59
src/app/file-card/file-card.component.css
Normal file
59
src/app/file-card/file-card.component.css
Normal file
@@ -0,0 +1,59 @@
|
||||
.example-card {
|
||||
width: 150px;
|
||||
height: 125px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.deleteButton {
|
||||
top:-5px;
|
||||
right:-5px;
|
||||
position:absolute;
|
||||
}
|
||||
|
||||
/* Coerce the <span> icon container away from display:inline */
|
||||
.mat-icon-button .mat-button-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.image {
|
||||
max-width:100%;
|
||||
max-height:100%;
|
||||
}
|
||||
|
||||
.example-full-width-height {
|
||||
width: 100%;
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.centered {
|
||||
margin: 0 auto;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.img-div {
|
||||
max-height: 80px;
|
||||
padding: 0px;
|
||||
margin: 0px 0px 0px -5px;
|
||||
width: calc(100% + 5px + 5px);
|
||||
}
|
||||
|
||||
.max-two-lines {
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
max-height: 2.4em;
|
||||
line-height: 1.2em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
@media (max-width: 576px){
|
||||
|
||||
.example-card {
|
||||
width: 125px !important;
|
||||
}
|
||||
|
||||
}
|
||||
17
src/app/file-card/file-card.component.html
Normal file
17
src/app/file-card/file-card.component.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<mat-card class="example-card mat-elevation-z6">
|
||||
<div style="padding:5px">
|
||||
<b><a href="javascript:void(0)" (click)="!isPlaylist ? mainComponent.goToFile(name, isAudio) : mainComponent.goToPlaylist(name, type)">{{title}}</a></b>
|
||||
<br/>
|
||||
<span class="max-two-lines">ID: {{name}}</span>
|
||||
<div *ngIf="isPlaylist">Count: {{count}}</div>
|
||||
<div *ngIf="!image_errored && thumbnailURL" class="img-div">
|
||||
<img class="image" (error) ="onImgError($event)" [id]="type" [lazyLoad]="thumbnailURL" [customObservable]="scrollAndLoad" (onLoad)="imageLoaded($event)" alt="Thumbnail">
|
||||
<span *ngIf="!image_loaded">
|
||||
<ngx-content-loading [width]="500" [height]="360">
|
||||
<svg:g ngx-rect width="500" height="360" y="0" x="0" rx="4" ry="4"></svg:g>
|
||||
</ngx-content-loading>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button (click)="deleteFile()" class="deleteButton" mat-icon-button><mat-icon>delete_forever</mat-icon></button>
|
||||
</mat-card>
|
||||
25
src/app/file-card/file-card.component.spec.ts
Normal file
25
src/app/file-card/file-card.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FileCardComponent } from './file-card.component';
|
||||
|
||||
describe('FileCardComponent', () => {
|
||||
let component: FileCardComponent;
|
||||
let fixture: ComponentFixture<FileCardComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ FileCardComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FileCardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
77
src/app/file-card/file-card.component.ts
Normal file
77
src/app/file-card/file-card.component.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Component, OnInit, Input, Output } from '@angular/core';
|
||||
import {PostsService} from '../posts.services';
|
||||
import {MatSnackBar} from '@angular/material';
|
||||
import {EventEmitter} from '@angular/core';
|
||||
import { MainComponent } from 'app/main/main.component';
|
||||
import { Subject, Observable } from 'rxjs';
|
||||
import 'rxjs/add/observable/merge';
|
||||
|
||||
@Component({
|
||||
selector: 'app-file-card',
|
||||
templateUrl: './file-card.component.html',
|
||||
styleUrls: ['./file-card.component.css']
|
||||
})
|
||||
export class FileCardComponent implements OnInit {
|
||||
|
||||
@Input() title: string;
|
||||
@Input() length: string;
|
||||
@Input() name: string;
|
||||
@Input() thumbnailURL: string;
|
||||
@Input() isAudio = true;
|
||||
@Output() removeFile: EventEmitter<string> = new EventEmitter<string>();
|
||||
@Input() isPlaylist = false;
|
||||
@Input() count = null;
|
||||
type;
|
||||
image_loaded = false;
|
||||
image_errored = false;
|
||||
|
||||
scrollSubject;
|
||||
scrollAndLoad;
|
||||
|
||||
constructor(private postsService: PostsService, public snackBar: MatSnackBar, public mainComponent: MainComponent) {
|
||||
this.scrollSubject = new Subject();
|
||||
this.scrollAndLoad = Observable.merge(
|
||||
Observable.fromEvent(window, 'scroll'),
|
||||
this.scrollSubject
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.type = this.isAudio ? 'audio' : 'video';
|
||||
}
|
||||
|
||||
deleteFile() {
|
||||
if (!this.isPlaylist) {
|
||||
this.postsService.deleteFile(this.name, this.isAudio).subscribe(result => {
|
||||
if (result === true) {
|
||||
this.openSnackBar('Delete success!', 'OK.');
|
||||
this.removeFile.emit(this.name);
|
||||
} else {
|
||||
this.openSnackBar('Delete failed!', 'OK.');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.removeFile.emit(this.name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onImgError(event) {
|
||||
this.image_errored = true;
|
||||
}
|
||||
|
||||
onHoverResponse() {
|
||||
this.scrollSubject.next();
|
||||
}
|
||||
|
||||
imageLoaded(loaded) {
|
||||
this.image_loaded = true;
|
||||
}
|
||||
|
||||
public openSnackBar(message: string, action: string) {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
3
src/app/input-dialog/input-dialog.component.css
Normal file
3
src/app/input-dialog/input-dialog.component.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.mat-spinner {
|
||||
margin-left: 5%;
|
||||
}
|
||||
16
src/app/input-dialog/input-dialog.component.html
Normal file
16
src/app/input-dialog/input-dialog.component.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<h4 mat-dialog-title>{{inputTitle}}</h4>
|
||||
<mat-dialog-content>
|
||||
<div>
|
||||
<mat-form-field color="accent">
|
||||
<input matInput (keyup.enter)="enterPressed()" [(ngModel)]="inputText" [placeholder]="inputPlaceholder">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button mat-dialog-close>Cancel</button>
|
||||
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
|
||||
<button mat-button [disabled]="!inputText" type="submit" (click)="enterPressed()">{{submitText}}</button>
|
||||
<div class="mat-spinner" *ngIf="inputSubmitted">
|
||||
<mat-spinner [diameter]="25"></mat-spinner>
|
||||
</div>
|
||||
</mat-dialog-actions>
|
||||
25
src/app/input-dialog/input-dialog.component.spec.ts
Normal file
25
src/app/input-dialog/input-dialog.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { InputDialogComponent } from './input-dialog.component';
|
||||
|
||||
describe('InputDialogComponent', () => {
|
||||
let component: InputDialogComponent;
|
||||
let fixture: ComponentFixture<InputDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ InputDialogComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(InputDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
50
src/app/input-dialog/input-dialog.component.ts
Normal file
50
src/app/input-dialog/input-dialog.component.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Component, OnInit, Input, Inject, EventEmitter } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
|
||||
|
||||
@Component({
|
||||
selector: 'app-input-dialog',
|
||||
templateUrl: './input-dialog.component.html',
|
||||
styleUrls: ['./input-dialog.component.css']
|
||||
})
|
||||
export class InputDialogComponent implements OnInit {
|
||||
|
||||
inputTitle: string;
|
||||
inputPlaceholder: string;
|
||||
submitText: string;
|
||||
|
||||
inputText = '';
|
||||
|
||||
inputSubmitted = false;
|
||||
|
||||
doneEmitter: EventEmitter<any> = null;
|
||||
onlyEmitOnDone = false;
|
||||
|
||||
constructor(public dialogRef: MatDialogRef<InputDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.inputTitle = this.data.inputTitle;
|
||||
this.inputPlaceholder = this.data.inputPlaceholder;
|
||||
this.submitText = this.data.submitText;
|
||||
|
||||
// checks if emitter exists, if so don't autoclose as it should be handled by caller
|
||||
if (this.data.doneEmitter) {
|
||||
this.doneEmitter = this.data.doneEmitter;
|
||||
this.onlyEmitOnDone = true;
|
||||
}
|
||||
}
|
||||
|
||||
enterPressed() {
|
||||
// validates input -- TODO: add custom validator
|
||||
if (this.inputText) {
|
||||
// only emit if emitter is passed
|
||||
if (this.onlyEmitOnDone) {
|
||||
this.doneEmitter.emit(this.inputText);
|
||||
this.inputSubmitted = true;
|
||||
} else {
|
||||
this.dialogRef.close(this.inputText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
122
src/app/main/main.component.css
Normal file
122
src/app/main/main.component.css
Normal file
@@ -0,0 +1,122 @@
|
||||
.demo-card {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.demo-basic {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.demo-basic .mat-card-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
mat-toolbar.top {
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/*::ng-deep .mat-form-field-placeholder{
|
||||
|
||||
transform: scale(.75) translateY(20px) !important;
|
||||
}*/
|
||||
|
||||
.big {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.centered {
|
||||
margin: 0 auto;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.example-full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.example-80-width {
|
||||
width: 80%
|
||||
}
|
||||
|
||||
mat-form-field.mat-form-field {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
margin-left: -28px;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.make-room-for-spinner {
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.equal-sizes {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.search-card-title {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.input-clear-button {
|
||||
position: absolute;
|
||||
right: -10px;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
.spinner-div {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: -40px;
|
||||
}
|
||||
|
||||
.margined {
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.results-div {
|
||||
position: relative;
|
||||
top: -15px;
|
||||
}
|
||||
|
||||
.first-result-card {
|
||||
border-radius: 4px 4px 0px 0px !important;
|
||||
}
|
||||
|
||||
.last-result-card {
|
||||
border-radius: 0px 0px 4px 4px !important;
|
||||
}
|
||||
|
||||
.only-result-card {
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
height: 120px;
|
||||
border-radius: 0px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.download-progress-bar {
|
||||
z-index: 999;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.add-playlist-button {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.advanced-input {
|
||||
width: 100%;
|
||||
}
|
||||
218
src/app/main/main.component.html
Normal file
218
src/app/main/main.component.html
Normal file
@@ -0,0 +1,218 @@
|
||||
<br/>
|
||||
<div class="big demo-basic">
|
||||
<mat-card id="card" style="margin-right: 20px; margin-left: 20px;">
|
||||
<mat-card-title>
|
||||
Youtube Downloader
|
||||
</mat-card-title>
|
||||
<mat-card-content>
|
||||
<div style="position: relative;">
|
||||
<form class="example-form">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div [ngClass]="allowQualitySelect ? 'col-sm-9' : null" class="col-12">
|
||||
<mat-form-field color="accent" class="example-full-width">
|
||||
<input style="padding-right: 25px;" matInput (ngModelChange)="inputChanged($event)" [(ngModel)]="url" [placeholder]="'URL' + (youtubeSearchEnabled ? ' or search' : '')" type="url" name="url" [formControl]="urlForm" required #urlinput>
|
||||
<mat-error *ngIf="urlError || urlForm.invalid">Please enter a valid URL!</mat-error>
|
||||
<button class="input-clear-button" mat-icon-button (click)="clearInput()"><mat-icon>clear</mat-icon></button>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div *ngIf="allowQualitySelect" class="col-7 col-sm-3">
|
||||
<mat-form-field color="accent" style="display: inline-block; width: inherit; min-width: 120px;">
|
||||
<mat-label>Quality</mat-label>
|
||||
<mat-select [ngModelOptions]="{standalone: true}" [(ngModel)]="selectedQuality">
|
||||
<ng-container *ngFor="let option of qualityOptions[(audioOnly) ? 'audio' : 'video']">
|
||||
<mat-option *ngIf="option.value === '' || url && cachedAvailableFormats[url] && cachedAvailableFormats[url]['formats'] && cachedAvailableFormats[url]['formats'][(audioOnly) ? 'audio' : 'video'][option.value]" [value]="option.value">
|
||||
{{option.label}}
|
||||
</mat-option>
|
||||
</ng-container>
|
||||
</mat-select>
|
||||
<div class="spinner-div" *ngIf="url !== '' && cachedAvailableFormats[url] && cachedAvailableFormats[url]['formats_loading']">
|
||||
<mat-spinner [diameter]="25"></mat-spinner>
|
||||
</div>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="results-div" *ngIf="results_showing">
|
||||
<span *ngFor="let result of results; let i = index">
|
||||
<mat-card class="result-card mat-elevation-z7" [ngClass]="[(i === 0 && results.length > 1) ? 'first-result-card' : '', ((i === results.length-1) && results.length > 1) ? 'last-result-card' : '', (results.length === 1) ? 'only-result-card' : '']">
|
||||
<div class="search-card-title">
|
||||
{{result.title}}
|
||||
</div>
|
||||
<div style="font-size: 12px; margin-bottom: 10px;">
|
||||
{{result.uploaded}}
|
||||
</div>
|
||||
<button mat-flat-button color="primary" style="float: left;" (click)="useURL(result.videoUrl)">Use URL</button>
|
||||
<button mat-stroked-button color="primary" (click)="visitURL(result.videoUrl)" style="float: right">View</button>
|
||||
</mat-card>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
<br/>
|
||||
<mat-checkbox [disabled]="current_download" (change)="videoModeChanged($event)" [(ngModel)]="audioOnly" style="float: left; margin-top: -12px">Only Audio</mat-checkbox>
|
||||
<mat-checkbox *ngIf="allowMultiDownloadMode" [disabled]="current_download" (change)="multiDownloadModeChanged($event)" [(ngModel)]="multiDownloadMode" style="float: right; margin-top: -12px">Multi-download mode</mat-checkbox>
|
||||
|
||||
</div>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button style="margin-left: 8px; margin-bottom: 8px" (click)="downloadClicked()" [disabled]="downloadingfile" type="submit" mat-stroked-button
|
||||
color="accent">Download</button>
|
||||
<button (click)="cancelDownload()" style="float: right" *ngIf="!!current_download" mat-stroked-button color="warn">Cancel</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</div>
|
||||
<div *ngIf="allowAdvancedDownload" class="big demo-basic">
|
||||
<form style="margin-left: 20px; margin-right: 20px;">
|
||||
<mat-expansion-panel class="big">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
Advanced
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<div class="container" style="padding-bottom: 20px;">
|
||||
<div class="row">
|
||||
<div class="col-12 col-sm-6">
|
||||
<mat-checkbox color="accent" [disabled]="current_download" (change)="customArgsEnabledChanged($event)" [(ngModel)]="customArgsEnabled" style="z-index: 999" [ngModelOptions]="{standalone: true}">Use custom args</mat-checkbox>
|
||||
<mat-form-field color="accent" style="margin-bottom: 42px;" class="advanced-input">
|
||||
<input [(ngModel)]="customArgs" [ngModelOptions]="{standalone: true}" [disabled]="!customArgsEnabled" matInput placeholder="Custom args">
|
||||
<mat-hint>No need to include URL, just everything after.</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6">
|
||||
<mat-checkbox color="accent" [disabled]="current_download" (change)="customOutputEnabledChanged($event)" [(ngModel)]="customOutputEnabled" style="z-index: 999" [ngModelOptions]="{standalone: true}">Use custom output</mat-checkbox>
|
||||
<mat-form-field style="margin-bottom: 42px;" color="accent" class="advanced-input">
|
||||
<input [(ngModel)]="customOutput" [ngModelOptions]="{standalone: true}" [disabled]="!customOutputEnabled" matInput placeholder="Custom output">
|
||||
<mat-hint><a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#output-template">Documentation</a>. Path is relative to the config download path. Don't include extension.</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div *ngIf="!youtubeAuthDisabledOverride" class="col-12 col-sm-6 mt-2">
|
||||
<mat-checkbox color="accent" [disabled]="current_download" (change)="youtubeAuthEnabledChanged($event)" [(ngModel)]="youtubeAuthEnabled" style="z-index: 999" [ngModelOptions]="{standalone: true}">Use authentication</mat-checkbox>
|
||||
<mat-form-field color="accent" class="advanced-input">
|
||||
<input [(ngModel)]="youtubeUsername" [ngModelOptions]="{standalone: true}" [disabled]="!youtubeAuthEnabled" matInput placeholder="Username">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div *ngIf="!youtubeAuthDisabledOverride" class="col-12 col-sm-6 mt-2">
|
||||
<mat-form-field style="margin-top: 31px;" color="accent" class="advanced-input">
|
||||
<input [(ngModel)]="youtubePassword" type="password" [ngModelOptions]="{standalone: true}" [disabled]="!youtubeAuthEnabled" matInput placeholder="Password">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
</form>
|
||||
</div>
|
||||
<div *ngIf="multiDownloadMode && downloads.length > 0 && !current_download" style="margin-top: 15px;" class="big demo-basic">
|
||||
<mat-card id="card" style="margin-right: 20px; margin-left: 20px;">
|
||||
<div class="container">
|
||||
<div *ngFor="let download of downloads; let i = index;" class="row">
|
||||
<ng-container *ngIf="current_download !== download">
|
||||
<app-download-item style="width: 100%" [download]="download" [queueNumber]="i+1" (cancelDownload)="cancelDownload($event)"></app-download-item>
|
||||
<mat-divider style="position: relative" *ngIf="i !== downloads.length - 1"></mat-divider>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="centered big" id="bar_div" *ngIf="current_download && current_download.downloading; else nofile">
|
||||
<div class="margined">
|
||||
<div [ngClass]="(determinateProgress && percentDownloaded === 100)?'make-room-for-spinner':'equal-sizes'" style="display: inline-block; width: 100%; padding-left: 20px" *ngIf="determinateProgress;else indeterminateprogress">
|
||||
<mat-progress-bar mode="determinate" value="{{percentDownloaded}}"></mat-progress-bar>
|
||||
<br/>
|
||||
</div>
|
||||
<div *ngIf="determinateProgress && percentDownloaded === 100" class="spinner">
|
||||
<mat-spinner [diameter]="25"></mat-spinner>
|
||||
</div>
|
||||
<ng-template #indeterminateprogress>
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
</ng-template>
|
||||
</div>
|
||||
<br/>
|
||||
</div>
|
||||
<ng-template #nofile>
|
||||
|
||||
</ng-template>
|
||||
<div style="margin: 20px" *ngIf="fileManagerEnabled">
|
||||
<mat-accordion>
|
||||
<mat-expansion-panel (opened)="accordionOpened('audio')" (closed)="accordionClosed('audio')" (mouseleave)="accordionLeft('audio')" (mouseenter)="accordionEntered('audio')" class="big">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
Audio
|
||||
</mat-panel-title>
|
||||
<mat-panel-description>
|
||||
Your audio files are here
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
<div *ngIf="mp3s.length > 0;else nomp3s">
|
||||
<mat-grid-list style="margin-bottom: 15px;" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
|
||||
<mat-grid-tile *ngFor="let file of mp3s; index as i;">
|
||||
<app-file-card #audiofilecard (removeFile)="removeFromMp3($event)" [title]="file.title" [name]="file.id" [thumbnailURL]="file.thumbnailURL"
|
||||
[length]="file.duration" [isAudio]="true"></app-file-card>
|
||||
<mat-progress-bar *ngIf="downloading_content['audio'][file.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
<mat-divider></mat-divider>
|
||||
<div style="width: 100%; text-align: center; margin-top: 10px;">
|
||||
<h6>Playlists</h6>
|
||||
</div>
|
||||
<mat-grid-list *ngIf="playlists.audio.length > 0" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
|
||||
<mat-grid-tile *ngFor="let playlist of playlists.audio; let i = index;">
|
||||
<app-file-card #audiofilecard (removeFile)="removePlaylistMp3(playlist.id, i)" [title]="playlist.name" [name]="playlist.id" [thumbnailURL]="playlist_thumbnails[playlist.id]"
|
||||
[length]="null" [isAudio]="true" [isPlaylist]="true" [count]="playlist.fileNames.length"></app-file-card>
|
||||
<mat-progress-bar *ngIf="downloading_content['audio'][playlist.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
<div class="add-playlist-button"><button (click)="openCreatePlaylistDialog('audio')" mat-fab><mat-icon>add</mat-icon></button></div>
|
||||
<div *ngIf="playlists.audio.length === 0">
|
||||
No playlists available. Create one from your downloading audio files by clicking the blue plus button.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</mat-expansion-panel>
|
||||
<mat-expansion-panel (opened)="accordionOpened('video')" (closed)="accordionClosed('video')" (mouseleave)="accordionLeft('video')" (mouseenter)="accordionEntered('video')" class="big">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
Video
|
||||
</mat-panel-title>
|
||||
<mat-panel-description>
|
||||
Your video files are here
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
<div *ngIf="mp4s.length > 0;else nomp4s">
|
||||
<mat-grid-list style="margin-bottom: 15px;" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
|
||||
<mat-grid-tile *ngFor="let file of mp4s; index as i;">
|
||||
<app-file-card #videofilecard (removeFile)="removeFromMp4($event)" [title]="file.title" [name]="file.id" [thumbnailURL]="file.thumbnailURL"
|
||||
[length]="file.duration" [isAudio]="false"></app-file-card>
|
||||
<mat-progress-bar *ngIf="downloading_content['video'][file.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<div style="width: 100%; text-align: center; margin-top: 10px;">
|
||||
<h6>Playlists</h6>
|
||||
</div>
|
||||
<mat-grid-list *ngIf="playlists.video.length > 0" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
|
||||
<mat-grid-tile *ngFor="let playlist of playlists.video; let i = index;">
|
||||
<app-file-card #videofilecard (removeFile)="removePlaylistMp4(playlist.id, i)" [title]="playlist.name" [name]="playlist.id" [thumbnailURL]="playlist_thumbnails[playlist.id]"
|
||||
[length]="null" [isAudio]="false" [isPlaylist]="true" [count]="playlist.fileNames.length"></app-file-card>
|
||||
<mat-progress-bar *ngIf="downloading_content['video'][playlist.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
|
||||
<!-- Add video playlist button -->
|
||||
<div class="add-playlist-button"><button (click)="openCreatePlaylistDialog('video')" mat-fab><mat-icon>add</mat-icon></button></div>
|
||||
<div *ngIf="playlists.video.length === 0">
|
||||
No playlists available. Create one from your downloading video files by clicking the blue plus button.
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
</div>
|
||||
|
||||
<ng-template #nomp3s>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template #nomp4s>
|
||||
|
||||
</ng-template>
|
||||
25
src/app/main/main.component.spec.ts
Normal file
25
src/app/main/main.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MainComponent } from './main.component';
|
||||
|
||||
describe('MainComponent', () => {
|
||||
let component: MainComponent;
|
||||
let fixture: ComponentFixture<MainComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ MainComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MainComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
965
src/app/main/main.component.ts
Normal file
965
src/app/main/main.component.ts
Normal file
@@ -0,0 +1,965 @@
|
||||
import { Component, OnInit, ElementRef, ViewChild, ViewChildren, QueryList, isDevMode } from '@angular/core';
|
||||
import {PostsService} from '../posts.services';
|
||||
import {FileCardComponent} from '../file-card/file-card.component';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import {FormControl, Validators} from '@angular/forms';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {MatSnackBar, MatDialog} from '@angular/material';
|
||||
import { saveAs } from 'file-saver';
|
||||
import 'rxjs/add/observable/of';
|
||||
import 'rxjs/add/operator/mapTo';
|
||||
import 'rxjs/add/operator/toPromise';
|
||||
import 'rxjs/add/observable/fromEvent'
|
||||
import 'rxjs/add/operator/filter'
|
||||
import 'rxjs/add/operator/debounceTime'
|
||||
import 'rxjs/add/operator/do'
|
||||
import 'rxjs/add/operator/switch'
|
||||
import { YoutubeSearchService, Result } from '../youtube-search.service';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { CreatePlaylistComponent } from 'app/create-playlist/create-playlist.component';
|
||||
import { Platform } from '@angular/cdk/platform';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export let audioFilesMouseHovering = false;
|
||||
export let videoFilesMouseHovering = false;
|
||||
export let audioFilesOpened = false;
|
||||
export let videoFilesOpened = false;
|
||||
|
||||
export interface Download {
|
||||
uid: string;
|
||||
type: string;
|
||||
url: string;
|
||||
percent_complete: number;
|
||||
downloading: boolean;
|
||||
is_playlist: boolean;
|
||||
fileNames?: string[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './main.component.html',
|
||||
styleUrls: ['./main.component.css']
|
||||
})
|
||||
export class MainComponent implements OnInit {
|
||||
youtubeAuthDisabledOverride = true;
|
||||
|
||||
iOS = false;
|
||||
|
||||
determinateProgress = false;
|
||||
downloadingfile = false;
|
||||
audioOnly: boolean;
|
||||
multiDownloadMode = false;
|
||||
customArgsEnabled = false;
|
||||
customArgs = null;
|
||||
customOutputEnabled = false;
|
||||
customOutput = null;
|
||||
youtubeAuthEnabled = false;
|
||||
youtubeUsername = null;
|
||||
youtubePassword = null;
|
||||
urlError = false;
|
||||
path = '';
|
||||
url = '';
|
||||
exists = '';
|
||||
percentDownloaded: number;
|
||||
autoStartDownload = false;
|
||||
|
||||
// settings
|
||||
fileManagerEnabled = false;
|
||||
allowQualitySelect = false;
|
||||
downloadOnlyMode = false;
|
||||
allowMultiDownloadMode = false;
|
||||
audioFolderPath;
|
||||
videoFolderPath;
|
||||
allowAdvancedDownload = false;
|
||||
|
||||
cachedAvailableFormats = {};
|
||||
|
||||
// youtube api
|
||||
youtubeSearchEnabled = false;
|
||||
youtubeAPIKey = null;
|
||||
results_loading = false;
|
||||
results_showing = true;
|
||||
results = [];
|
||||
|
||||
mp3s: any[] = [];
|
||||
mp4s: any[] = [];
|
||||
files_cols = null;
|
||||
playlists = {'audio': [], 'video': []};
|
||||
playlist_thumbnails = {};
|
||||
downloading_content = {'audio': {}, 'video': {}};
|
||||
downloads: Download[] = [];
|
||||
current_download: Download = null;
|
||||
|
||||
urlForm = new FormControl('', [Validators.required]);
|
||||
|
||||
qualityOptions = {
|
||||
'video': [
|
||||
{
|
||||
'resolution': null,
|
||||
'value': '',
|
||||
'label': 'Max'
|
||||
},
|
||||
{
|
||||
'resolution': '3840x2160',
|
||||
'value': '2160',
|
||||
'label': '2160p (4K)'
|
||||
},
|
||||
{
|
||||
'resolution': '2560x1440',
|
||||
'value': '1440',
|
||||
'label': '1440p'
|
||||
},
|
||||
{
|
||||
'resolution': '1920x1080',
|
||||
'value': '1080',
|
||||
'label': '1080p'
|
||||
},
|
||||
{
|
||||
'resolution': '1280x720',
|
||||
'value': '720',
|
||||
'label': '720p'
|
||||
},
|
||||
{
|
||||
'resolution': '720x480',
|
||||
'value': '480',
|
||||
'label': '480p'
|
||||
},
|
||||
{
|
||||
'resolution': '480x360',
|
||||
'value': '360',
|
||||
'label': '360p'
|
||||
},
|
||||
{
|
||||
'resolution': '360x240',
|
||||
'value': '240',
|
||||
'label': '240p'
|
||||
},
|
||||
{
|
||||
'resolution': '256x144',
|
||||
'value': '144',
|
||||
'label': '144p'
|
||||
}
|
||||
],
|
||||
'audio': [
|
||||
{
|
||||
'kbitrate': null,
|
||||
'value': '',
|
||||
'label': 'Max'
|
||||
},
|
||||
{
|
||||
'kbitrate': '256',
|
||||
'value': '256K',
|
||||
'label': '256 Kbps'
|
||||
},
|
||||
{
|
||||
'kbitrate': '160',
|
||||
'value': '160K',
|
||||
'label': '160 Kbps'
|
||||
},
|
||||
{
|
||||
'kbitrate': '128',
|
||||
'value': '128K',
|
||||
'label': '128 Kbps'
|
||||
},
|
||||
{
|
||||
'kbitrate': '96',
|
||||
'value': '96K',
|
||||
'label': '96 Kbps'
|
||||
},
|
||||
{
|
||||
'kbitrate': '70',
|
||||
'value': '70K',
|
||||
'label': '70 Kbps'
|
||||
},
|
||||
{
|
||||
'kbitrate': '50',
|
||||
'value': '50K',
|
||||
'label': '50 Kbps'
|
||||
},
|
||||
{
|
||||
'kbitrate': '32',
|
||||
'value': '32K',
|
||||
'label': '32 Kbps'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
selectedQuality = '';
|
||||
formats_loading = false;
|
||||
|
||||
@ViewChild('urlinput', { read: ElementRef, static: false }) urlInput: ElementRef;
|
||||
@ViewChildren('audiofilecard') audioFileCards: QueryList<FileCardComponent>;
|
||||
@ViewChildren('videofilecard') videoFileCards: QueryList<FileCardComponent>;
|
||||
last_valid_url = '';
|
||||
last_url_check = 0;
|
||||
|
||||
test_download: Download = {
|
||||
uid: null,
|
||||
type: 'audio',
|
||||
percent_complete: 0,
|
||||
url: 'http://youtube.com/watch?v=17848rufj',
|
||||
downloading: true,
|
||||
is_playlist: false
|
||||
};
|
||||
|
||||
constructor(private postsService: PostsService, private youtubeSearch: YoutubeSearchService, public snackBar: MatSnackBar,
|
||||
private router: Router, public dialog: MatDialog, private platform: Platform, private route: ActivatedRoute) {
|
||||
this.audioOnly = false;
|
||||
|
||||
// loading config
|
||||
this.postsService.loadNavItems().subscribe(res => { // loads settings
|
||||
const result = !this.postsService.debugMode ? res['config_file'] : res;
|
||||
this.fileManagerEnabled = result['YoutubeDLMaterial']['Extra']['file_manager_enabled'];
|
||||
this.downloadOnlyMode = result['YoutubeDLMaterial']['Extra']['download_only_mode'];
|
||||
this.allowMultiDownloadMode = result['YoutubeDLMaterial']['Extra']['allow_multi_download_mode'];
|
||||
this.audioFolderPath = result['YoutubeDLMaterial']['Downloader']['path-audio'];
|
||||
this.videoFolderPath = result['YoutubeDLMaterial']['Downloader']['path-video'];
|
||||
this.youtubeSearchEnabled = result['YoutubeDLMaterial']['API'] && result['YoutubeDLMaterial']['API']['use_youtube_API'] &&
|
||||
result['YoutubeDLMaterial']['API']['youtube_API_key'];
|
||||
this.youtubeAPIKey = this.youtubeSearchEnabled ? result['YoutubeDLMaterial']['API']['youtube_API_key'] : null;
|
||||
this.allowQualitySelect = result['YoutubeDLMaterial']['Extra']['allow_quality_select'];
|
||||
this.allowAdvancedDownload = result['YoutubeDLMaterial']['Advanced']['allow_advanced_download'];
|
||||
|
||||
|
||||
if (this.fileManagerEnabled) {
|
||||
this.getMp3s();
|
||||
this.getMp4s();
|
||||
}
|
||||
|
||||
if (this.youtubeSearchEnabled && this.youtubeAPIKey) {
|
||||
this.youtubeSearch.initializeAPI(this.youtubeAPIKey);
|
||||
this.attachToInput();
|
||||
}
|
||||
|
||||
// set final cache items
|
||||
if (this.allowAdvancedDownload) {
|
||||
if (localStorage.getItem('customArgsEnabled') !== null) {
|
||||
this.customArgsEnabled = localStorage.getItem('customArgsEnabled') === 'true';
|
||||
}
|
||||
|
||||
if (localStorage.getItem('customOutputEnabled') !== null) {
|
||||
this.customOutputEnabled = localStorage.getItem('customOutputEnabled') === 'true';
|
||||
}
|
||||
|
||||
if (localStorage.getItem('youtubeAuthEnabled') !== null) {
|
||||
this.youtubeAuthEnabled = localStorage.getItem('youtubeAuthEnabled') === 'true';
|
||||
}
|
||||
|
||||
// set advanced inputs
|
||||
const customArgs = localStorage.getItem('customArgs');
|
||||
const customOutput = localStorage.getItem('customOutput');
|
||||
const youtubeUsername = localStorage.getItem('youtubeUsername');
|
||||
|
||||
if (customArgs && customArgs !== 'null') { this.customArgs = customArgs };
|
||||
if (customOutput && customOutput !== 'null') { this.customOutput = customOutput };
|
||||
if (youtubeUsername && youtubeUsername !== 'null') { this.youtubeUsername = youtubeUsername };
|
||||
}
|
||||
|
||||
if (this.autoStartDownload) {
|
||||
this.downloadClicked();
|
||||
}
|
||||
|
||||
}, error => {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// app initialization.
|
||||
ngOnInit() {
|
||||
this.iOS = this.platform.IOS;
|
||||
|
||||
// get checkboxes
|
||||
if (localStorage.getItem('audioOnly') !== null) {
|
||||
this.audioOnly = localStorage.getItem('audioOnly') === 'true';
|
||||
}
|
||||
|
||||
if (localStorage.getItem('multiDownloadMode') !== null) {
|
||||
this.multiDownloadMode = localStorage.getItem('multiDownloadMode') === 'true';
|
||||
}
|
||||
|
||||
// check if params exist
|
||||
if (this.route.snapshot.paramMap.get('url')) {
|
||||
this.url = decodeURIComponent(this.route.snapshot.paramMap.get('url'));
|
||||
this.audioOnly = this.route.snapshot.paramMap.get('audioOnly') === 'true';
|
||||
|
||||
// set auto start flag to true
|
||||
this.autoStartDownload = true;
|
||||
}
|
||||
|
||||
this.setCols();
|
||||
}
|
||||
|
||||
// file manager stuff
|
||||
|
||||
getMp3s() {
|
||||
this.postsService.getMp3s().subscribe(result => {
|
||||
const mp3s = result['mp3s'];
|
||||
const playlists = result['playlists'];
|
||||
// if they are different
|
||||
if (JSON.stringify(this.mp3s) !== JSON.stringify(mp3s)) { this.mp3s = mp3s };
|
||||
this.playlists.audio = playlists;
|
||||
|
||||
// get thumbnail url by using first video. this is a temporary hack
|
||||
for (let i = 0; i < this.playlists.audio.length; i++) {
|
||||
const playlist = this.playlists.audio[i];
|
||||
let videoToExtractThumbnail = null;
|
||||
for (let j = 0; j < this.mp3s.length; j++) {
|
||||
if (this.mp3s[j].id === playlist.fileNames[0]) {
|
||||
// found the corresponding file
|
||||
videoToExtractThumbnail = this.mp3s[j];
|
||||
}
|
||||
}
|
||||
|
||||
if (videoToExtractThumbnail) { this.playlist_thumbnails[playlist.id] = videoToExtractThumbnail.thumbnailURL; }
|
||||
}
|
||||
}, error => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
getMp4s() {
|
||||
this.postsService.getMp4s().subscribe(result => {
|
||||
const mp4s = result['mp4s'];
|
||||
const playlists = result['playlists'];
|
||||
// if they are different
|
||||
if (JSON.stringify(this.mp4s) !== JSON.stringify(mp4s)) { this.mp4s = mp4s };
|
||||
this.playlists.video = playlists;
|
||||
|
||||
// get thumbnail url by using first video. this is a temporary hack
|
||||
for (let i = 0; i < this.playlists.video.length; i++) {
|
||||
const playlist = this.playlists.video[i];
|
||||
let videoToExtractThumbnail = null;
|
||||
for (let j = 0; j < this.mp4s.length; j++) {
|
||||
if (this.mp4s[j].id === playlist.fileNames[0]) {
|
||||
// found the corresponding file
|
||||
videoToExtractThumbnail = this.mp4s[j];
|
||||
}
|
||||
}
|
||||
|
||||
if (videoToExtractThumbnail) { this.playlist_thumbnails[playlist.id] = videoToExtractThumbnail.thumbnailURL; }
|
||||
}
|
||||
},
|
||||
error => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
public setCols() {
|
||||
if (window.innerWidth <= 350) {
|
||||
this.files_cols = 1;
|
||||
} else if (window.innerWidth <= 500) {
|
||||
this.files_cols = 2;
|
||||
} else if (window.innerWidth <= 750) {
|
||||
this.files_cols = 3
|
||||
} else {
|
||||
this.files_cols = 4;
|
||||
}
|
||||
}
|
||||
|
||||
public goToFile(name, isAudio) {
|
||||
if (isAudio) {
|
||||
this.downloadHelperMp3(name, false, false);
|
||||
} else {
|
||||
this.downloadHelperMp4(name, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
public goToPlaylist(playlistID, type) {
|
||||
const playlist = this.getPlaylistObjectByID(playlistID, type);
|
||||
if (playlist) {
|
||||
if (this.downloadOnlyMode) {
|
||||
this.downloading_content[type][playlistID] = true;
|
||||
this.downloadPlaylist(playlist.fileNames, type, playlist.name, playlistID);
|
||||
} else {
|
||||
const fileNames = playlist.fileNames;
|
||||
this.router.navigate(['/player', {fileNames: fileNames.join('|nvr|'), type: type, id: playlistID}]);
|
||||
}
|
||||
} else {
|
||||
// playlist not found
|
||||
console.error(`Playlist with ID ${playlistID} not found!`);
|
||||
}
|
||||
}
|
||||
|
||||
getPlaylistObjectByID(playlistID, type) {
|
||||
for (let i = 0; i < this.playlists[type].length; i++) {
|
||||
const playlist = this.playlists[type][i];
|
||||
if (playlist.id === playlistID) {
|
||||
return playlist;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public removeFromMp3(name: string) {
|
||||
for (let i = 0; i < this.mp3s.length; i++) {
|
||||
if (this.mp3s[i].id === name) {
|
||||
this.mp3s.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public removePlaylistMp3(playlistID, index) {
|
||||
this.postsService.removePlaylist(playlistID, 'audio').subscribe(res => {
|
||||
if (res['success']) {
|
||||
this.playlists.audio.splice(index, 1);
|
||||
this.openSnackBar('Playlist successfully removed.', '');
|
||||
}
|
||||
this.getMp3s();
|
||||
});
|
||||
}
|
||||
|
||||
public removeFromMp4(name: string) {
|
||||
for (let i = 0; i < this.mp4s.length; i++) {
|
||||
if (this.mp4s[i].id === name) {
|
||||
this.mp4s.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public removePlaylistMp4(playlistID, index) {
|
||||
this.postsService.removePlaylist(playlistID, 'video').subscribe(res => {
|
||||
if (res['success']) {
|
||||
this.playlists.video.splice(index, 1);
|
||||
this.openSnackBar('Playlist successfully removed.', '');
|
||||
}
|
||||
this.getMp4s();
|
||||
});
|
||||
}
|
||||
|
||||
// download helpers
|
||||
|
||||
downloadHelperMp3(name, is_playlist = false, forceView = false, new_download = null) {
|
||||
this.downloadingfile = false;
|
||||
|
||||
if (new_download && this.current_download !== new_download) {
|
||||
// console.log('mismatched downloads');
|
||||
} else if (!this.multiDownloadMode || !new_download) {
|
||||
// if download only mode, just download the file. no redirect
|
||||
if (forceView === false && this.downloadOnlyMode && !this.iOS) {
|
||||
if (is_playlist) {
|
||||
const zipName = name[0].split(' ')[0] + name[1].split(' ')[0];
|
||||
this.downloadPlaylist(name, 'audio', zipName);
|
||||
} else {
|
||||
this.downloadAudioFile(decodeURI(name));
|
||||
}
|
||||
} else {
|
||||
if (is_playlist) {
|
||||
this.router.navigate(['/player', {fileNames: name.join('|nvr|'), type: 'audio'}]);
|
||||
} else {
|
||||
this.router.navigate(['/player', {fileNames: name, type: 'audio'}]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove download from current downloads
|
||||
this.removeDownloadFromCurrentDownloads(new_download);
|
||||
|
||||
// reloads mp3s
|
||||
if (this.fileManagerEnabled) {
|
||||
this.getMp3s();
|
||||
setTimeout(() => {
|
||||
this.audioFileCards.forEach(filecard => {
|
||||
filecard.onHoverResponse();
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
downloadHelperMp4(name, is_playlist = false, forceView = false, new_download = null) {
|
||||
this.downloadingfile = false;
|
||||
|
||||
if (new_download && this.current_download !== new_download) {
|
||||
// console.log('mismatched downloads');
|
||||
} else if (!this.multiDownloadMode || !new_download) {
|
||||
// if download only mode, just download the file. no redirect
|
||||
if (forceView === false && this.downloadOnlyMode) {
|
||||
if (is_playlist) {
|
||||
const zipName = name[0].split(' ')[0] + name[1].split(' ')[0];
|
||||
this.downloadPlaylist(name, 'video', zipName);
|
||||
} else {
|
||||
this.downloadVideoFile(decodeURI(name));
|
||||
}
|
||||
} else {
|
||||
if (is_playlist) {
|
||||
this.router.navigate(['/player', {fileNames: name.join('|nvr|'), type: 'video'}]);
|
||||
} else {
|
||||
this.router.navigate(['/player', {fileNames: name, type: 'video'}]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove download from current downloads
|
||||
this.removeDownloadFromCurrentDownloads(new_download);
|
||||
|
||||
// reloads mp4s
|
||||
if (this.fileManagerEnabled) {
|
||||
this.getMp4s();
|
||||
setTimeout(() => {
|
||||
this.videoFileCards.forEach(filecard => {
|
||||
filecard.onHoverResponse();
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
// download click handler
|
||||
downloadClicked() {
|
||||
if (this.ValidURL(this.url)) {
|
||||
this.urlError = false;
|
||||
this.path = '';
|
||||
|
||||
// get common args
|
||||
const customArgs = (this.customArgsEnabled ? this.customArgs : null);
|
||||
const customOutput = (this.customOutputEnabled ? this.customOutput : null);
|
||||
const youtubeUsername = (this.youtubeAuthEnabled && this.youtubeUsername ? this.youtubeUsername : null);
|
||||
const youtubePassword = (this.youtubeAuthEnabled && this.youtubePassword ? this.youtubePassword : null);
|
||||
|
||||
// set advanced inputs
|
||||
if (this.allowAdvancedDownload) {
|
||||
if (customArgs) {
|
||||
localStorage.setItem('customArgs', customArgs);
|
||||
}
|
||||
if (customOutput) {
|
||||
localStorage.setItem('customOutput', customOutput);
|
||||
}
|
||||
if (youtubeUsername) {
|
||||
localStorage.setItem('youtubeUsername', youtubeUsername);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.audioOnly) {
|
||||
// create download object
|
||||
const new_download: Download = {
|
||||
uid: uuid(),
|
||||
type: 'audio',
|
||||
percent_complete: 0,
|
||||
url: this.url,
|
||||
downloading: true,
|
||||
is_playlist: this.url.includes('playlist')
|
||||
};
|
||||
this.downloads.push(new_download);
|
||||
if (!this.current_download && !this.multiDownloadMode) { this.current_download = new_download };
|
||||
this.downloadingfile = true;
|
||||
|
||||
let customQualityConfiguration = null;
|
||||
if (this.selectedQuality !== '') {
|
||||
const cachedFormatsExists = this.cachedAvailableFormats[this.url] && this.cachedAvailableFormats[this.url]['formats'];
|
||||
if (cachedFormatsExists) {
|
||||
const audio_formats = this.cachedAvailableFormats[this.url]['formats']['audio'];
|
||||
customQualityConfiguration = audio_formats[this.selectedQuality]['format_id'];
|
||||
}
|
||||
}
|
||||
|
||||
this.postsService.makeMP3(this.url, (this.selectedQuality === '' ? null : this.selectedQuality),
|
||||
customQualityConfiguration, customArgs, customOutput, youtubeUsername, youtubePassword).subscribe(posts => {
|
||||
// update download object
|
||||
new_download.downloading = false;
|
||||
new_download.percent_complete = 100;
|
||||
|
||||
const is_playlist = !!(posts['file_names']);
|
||||
this.path = is_playlist ? posts['file_names'] : posts['audiopathEncoded'];
|
||||
|
||||
if (this.path !== '-1') {
|
||||
this.downloadHelperMp3(this.path, is_playlist, false, new_download);
|
||||
}
|
||||
}, error => { // can't access server
|
||||
this.downloadingfile = false;
|
||||
this.openSnackBar('Download failed!', 'OK.');
|
||||
});
|
||||
} else {
|
||||
// create download object
|
||||
const new_download: Download = {
|
||||
uid: uuid(),
|
||||
type: 'video',
|
||||
percent_complete: 0,
|
||||
url: this.url,
|
||||
downloading: true,
|
||||
is_playlist: this.url.includes('playlist')
|
||||
};
|
||||
this.downloads.push(new_download);
|
||||
if (!this.current_download && !this.multiDownloadMode) { this.current_download = new_download };
|
||||
this.downloadingfile = true;
|
||||
|
||||
let customQualityConfiguration = null;
|
||||
const cachedFormatsExists = this.cachedAvailableFormats[this.url] && this.cachedAvailableFormats[this.url]['formats'];
|
||||
if (cachedFormatsExists) {
|
||||
const video_formats = this.cachedAvailableFormats[this.url]['formats']['video'];
|
||||
if (video_formats['best_audio_format'] && this.selectedQuality !== '') {
|
||||
customQualityConfiguration = video_formats[this.selectedQuality]['format_id'] + '+' + video_formats['best_audio_format'];
|
||||
}
|
||||
}
|
||||
|
||||
this.postsService.makeMP4(this.url, (this.selectedQuality === '' ? null : this.selectedQuality),
|
||||
customQualityConfiguration, customArgs, customOutput, youtubeUsername, youtubePassword).subscribe(posts => {
|
||||
// update download object
|
||||
new_download.downloading = false;
|
||||
new_download.percent_complete = 100;
|
||||
|
||||
const is_playlist = !!(posts['file_names']);
|
||||
this.path = is_playlist ? posts['file_names'] : posts['videopathEncoded'];
|
||||
|
||||
if (this.path !== '-1') {
|
||||
this.downloadHelperMp4(this.path, is_playlist, false, new_download);
|
||||
}
|
||||
}, error => { // can't access server
|
||||
this.downloadingfile = false;
|
||||
this.openSnackBar('Download failed!', 'OK.');
|
||||
});
|
||||
}
|
||||
|
||||
if (this.multiDownloadMode) {
|
||||
this.url = '';
|
||||
this.downloadingfile = false;
|
||||
}
|
||||
} else {
|
||||
this.urlError = true;
|
||||
}
|
||||
}
|
||||
|
||||
// download canceled handler
|
||||
cancelDownload(download_to_cancel = null) {
|
||||
// if one is provided, cancel that one. otherwise, remove the current one
|
||||
if (download_to_cancel) {
|
||||
this.removeDownloadFromCurrentDownloads(download_to_cancel)
|
||||
return;
|
||||
}
|
||||
this.downloadingfile = false;
|
||||
this.current_download.downloading = false;
|
||||
this.current_download = null;
|
||||
}
|
||||
|
||||
getDownloadByUID(uid) {
|
||||
const index = this.downloads.findIndex(download => download.uid === uid);
|
||||
if (index !== -1) {
|
||||
return this.downloads[index];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
removeDownloadFromCurrentDownloads(download_to_remove) {
|
||||
const index = this.downloads.indexOf(download_to_remove);
|
||||
if (index !== -1) {
|
||||
this.downloads.splice(index, 1);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
downloadAudioFile(name) {
|
||||
this.downloading_content['audio'][name] = true;
|
||||
this.postsService.downloadFileFromServer(name, 'audio').subscribe(res => {
|
||||
this.downloading_content['audio'][name] = false;
|
||||
const blob: Blob = res;
|
||||
saveAs(blob, name + '.mp3');
|
||||
|
||||
if (!this.fileManagerEnabled) {
|
||||
// tell server to delete the file once downloaded
|
||||
this.postsService.deleteFile(name, true).subscribe(delRes => {
|
||||
// reload mp3s
|
||||
this.getMp3s();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
downloadVideoFile(name) {
|
||||
this.downloading_content['video'][name] = true;
|
||||
this.postsService.downloadFileFromServer(name, 'video').subscribe(res => {
|
||||
this.downloading_content['video'][name] = false;
|
||||
const blob: Blob = res;
|
||||
saveAs(blob, name + '.mp4');
|
||||
|
||||
if (!this.fileManagerEnabled) {
|
||||
// tell server to delete the file once downloaded
|
||||
this.postsService.deleteFile(name, false).subscribe(delRes => {
|
||||
// reload mp4s
|
||||
this.getMp4s();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
downloadPlaylist(fileNames, type, zipName = null, playlistID = null) {
|
||||
this.postsService.downloadFileFromServer(fileNames, type, zipName).subscribe(res => {
|
||||
if (playlistID) { this.downloading_content[type][playlistID] = false };
|
||||
const blob: Blob = res;
|
||||
saveAs(blob, zipName + '.zip');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
clearInput() {
|
||||
this.url = '';
|
||||
this.results_showing = false;
|
||||
}
|
||||
|
||||
onInputBlur() {
|
||||
this.results_showing = false;
|
||||
}
|
||||
|
||||
visitURL(url) {
|
||||
window.open(url);
|
||||
}
|
||||
|
||||
useURL(url) {
|
||||
this.results_showing = false;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
inputChanged(new_val) {
|
||||
if (new_val === '' || !new_val) {
|
||||
this.results_showing = false;
|
||||
} else {
|
||||
if (this.ValidURL(new_val)) {
|
||||
this.results_showing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checks if url is a valid URL
|
||||
ValidURL(str) {
|
||||
// tslint:disable-next-line: max-line-length
|
||||
const strRegex = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/;
|
||||
const re = new RegExp(strRegex);
|
||||
const valid = re.test(str);
|
||||
|
||||
if (!valid) { return false; }
|
||||
|
||||
// tslint:disable-next-line: max-line-length
|
||||
const youtubeStrRegex = /(?:http(?:s)?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:(?:watch)?\?(?:.*&)?v(?:i)?=|(?:embed|v|vi|user)\/))([^\?&\"'<> #]+)/;
|
||||
const reYT = new RegExp(youtubeStrRegex);
|
||||
const ytValid = true || reYT.test(str);
|
||||
if (valid && ytValid && Date.now() - this.last_url_check > 1000) {
|
||||
if (str !== this.last_valid_url && this.allowQualitySelect) {
|
||||
// get info
|
||||
this.getURLInfo(str);
|
||||
}
|
||||
this.last_valid_url = str;
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
// snackbar helper
|
||||
public openSnackBar(message: string, action: string) {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
getURLInfo(url) {
|
||||
if (!this.cachedAvailableFormats[url]) {
|
||||
this.cachedAvailableFormats[url] = {};
|
||||
}
|
||||
if (!(this.cachedAvailableFormats[url] && this.cachedAvailableFormats[url]['formats'])) {
|
||||
this.cachedAvailableFormats[url]['formats_loading'] = true;
|
||||
this.postsService.getFileInfo([url], 'irrelevant', true).subscribe(res => {
|
||||
this.cachedAvailableFormats[url]['formats_loading'] = false;
|
||||
const infos = res['result'];
|
||||
if (!infos || !infos.formats) {
|
||||
this.errorFormats(url);
|
||||
return;
|
||||
}
|
||||
const parsed_infos = this.getAudioAndVideoFormats(infos.formats);
|
||||
const available_formats = {audio: parsed_infos[0], video: parsed_infos[1]};
|
||||
this.cachedAvailableFormats[url]['formats'] = available_formats;
|
||||
}, err => {
|
||||
this.errorFormats(url);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
errorFormats(url) {
|
||||
this.cachedAvailableFormats[url]['formats_loading'] = false;
|
||||
console.error('Could not load formats for url ' + url);
|
||||
}
|
||||
|
||||
attachToInput() {
|
||||
Observable.fromEvent(this.urlInput.nativeElement, 'keyup')
|
||||
.map((e: any) => e.target.value) // extract the value of input
|
||||
.filter((text: string) => text.length > 1) // filter out if empty
|
||||
.debounceTime(250) // only once every 250ms
|
||||
.do(() => this.results_loading = true) // enable loading
|
||||
.map((query: string) => this.youtubeSearch.search(query))
|
||||
.switch() // act on the return of the search
|
||||
.subscribe(
|
||||
(results: Result[]) => {
|
||||
this.results_loading = false;
|
||||
if (this.url !== '' && results && results.length > 0) {
|
||||
this.results = results;
|
||||
this.results_showing = true;
|
||||
} else {
|
||||
this.results_showing = false;
|
||||
}
|
||||
},
|
||||
(err: any) => {
|
||||
console.log(err)
|
||||
this.results_loading = false;
|
||||
this.results_showing = false;
|
||||
},
|
||||
() => { // on completion
|
||||
this.results_loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onResize(event) {
|
||||
this.setCols();
|
||||
}
|
||||
|
||||
videoModeChanged(new_val) {
|
||||
this.selectedQuality = '';
|
||||
localStorage.setItem('audioOnly', new_val.checked.toString());
|
||||
}
|
||||
|
||||
multiDownloadModeChanged(new_val) {
|
||||
localStorage.setItem('multiDownloadMode', new_val.checked.toString());
|
||||
}
|
||||
|
||||
customArgsEnabledChanged(new_val) {
|
||||
localStorage.setItem('customArgsEnabled', new_val.checked.toString());
|
||||
if (new_val.checked === true && this.customOutputEnabled) {
|
||||
this.customOutputEnabled = false;
|
||||
localStorage.setItem('customOutputEnabled', 'false');
|
||||
|
||||
this.youtubeAuthEnabled = false;
|
||||
localStorage.setItem('youtubeAuthEnabled', 'false');
|
||||
}
|
||||
}
|
||||
|
||||
customOutputEnabledChanged(new_val) {
|
||||
localStorage.setItem('customOutputEnabled', new_val.checked.toString());
|
||||
if (new_val.checked === true && this.customArgsEnabled) {
|
||||
this.customArgsEnabled = false;
|
||||
localStorage.setItem('customArgsEnabled', 'false');
|
||||
}
|
||||
}
|
||||
|
||||
youtubeAuthEnabledChanged(new_val) {
|
||||
localStorage.setItem('youtubeAuthEnabled', new_val.checked.toString());
|
||||
if (new_val.checked === true && this.customArgsEnabled) {
|
||||
this.customArgsEnabled = false;
|
||||
localStorage.setItem('customArgsEnabled', 'false');
|
||||
}
|
||||
}
|
||||
|
||||
getAudioAndVideoFormats(formats): any[] {
|
||||
const audio_formats = {};
|
||||
const video_formats = {};
|
||||
|
||||
for (let i = 0; i < formats.length; i++) {
|
||||
const format_obj = {type: null};
|
||||
|
||||
const format = formats[i];
|
||||
const format_type = (format.vcodec === 'none') ? 'audio' : 'video';
|
||||
|
||||
format_obj.type = format_type;
|
||||
if (format_obj.type === 'audio' && format.abr) {
|
||||
const key = format.abr.toString() + 'K';
|
||||
format_obj['bitrate'] = format.abr;
|
||||
format_obj['format_id'] = format.format_id;
|
||||
format_obj['ext'] = format.ext;
|
||||
// don't overwrite if not m4a
|
||||
if (audio_formats[key]) {
|
||||
if (format.ext === 'm4a') {
|
||||
audio_formats[key] = format_obj;
|
||||
}
|
||||
} else {
|
||||
audio_formats[key] = format_obj;
|
||||
}
|
||||
} else if (format_obj.type === 'video') {
|
||||
// check if video format is mp4
|
||||
const key = format.height.toString();
|
||||
if (format.ext === 'mp4') {
|
||||
format_obj['height'] = format.height;
|
||||
format_obj['acodec'] = format.acodec;
|
||||
format_obj['format_id'] = format.format_id;
|
||||
|
||||
// no acodec means no overwrite
|
||||
if (!(video_formats[key]) || format_obj['acodec'] !== 'none') {
|
||||
video_formats[key] = format_obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
video_formats['best_audio_format'] = this.getBestAudioFormatForMp4(audio_formats);
|
||||
|
||||
return [audio_formats, video_formats]
|
||||
}
|
||||
|
||||
getBestAudioFormatForMp4(audio_formats) {
|
||||
let best_audio_format_for_mp4 = null;
|
||||
let best_audio_format_bitrate = 0;
|
||||
const available_audio_format_keys = Object.keys(audio_formats);
|
||||
for (let i = 0; i < available_audio_format_keys.length; i++) {
|
||||
const audio_format_key = available_audio_format_keys[i];
|
||||
const audio_format = audio_formats[audio_format_key];
|
||||
const is_m4a = audio_format.ext === 'm4a';
|
||||
if (is_m4a && audio_format.bitrate > best_audio_format_bitrate) {
|
||||
best_audio_format_for_mp4 = audio_format.format_id;
|
||||
best_audio_format_bitrate = audio_format.bitrate;
|
||||
}
|
||||
}
|
||||
return best_audio_format_for_mp4;
|
||||
}
|
||||
|
||||
accordionEntered(type) {
|
||||
if (type === 'audio') {
|
||||
audioFilesMouseHovering = true;
|
||||
this.audioFileCards.forEach(filecard => {
|
||||
filecard.onHoverResponse();
|
||||
});
|
||||
} else if (type === 'video') {
|
||||
videoFilesMouseHovering = true;
|
||||
this.videoFileCards.forEach(filecard => {
|
||||
filecard.onHoverResponse();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
accordionLeft(type) {
|
||||
if (type === 'audio') {
|
||||
audioFilesMouseHovering = false;
|
||||
} else if (type === 'video') {
|
||||
videoFilesMouseHovering = false;
|
||||
}
|
||||
}
|
||||
|
||||
accordionOpened(type) {
|
||||
if (type === 'audio') {
|
||||
audioFilesOpened = true;
|
||||
} else if (type === 'video') {
|
||||
videoFilesOpened = true;
|
||||
}
|
||||
}
|
||||
|
||||
accordionClosed(type) {
|
||||
if (type === 'audio') {
|
||||
audioFilesOpened = false;
|
||||
} else if (type === 'video') {
|
||||
videoFilesOpened = false;
|
||||
}
|
||||
}
|
||||
|
||||
// creating a playlist
|
||||
openCreatePlaylistDialog(type) {
|
||||
const dialogRef = this.dialog.open(CreatePlaylistComponent, {
|
||||
data: {
|
||||
filesToSelectFrom: (type === 'audio') ? this.mp3s : this.mp4s,
|
||||
type: type
|
||||
}
|
||||
});
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
if (type === 'audio') { this.getMp3s() };
|
||||
if (type === 'video') { this.getMp4s() };
|
||||
this.openSnackBar('Successfully created playlist!', '');
|
||||
} else if (result === false) {
|
||||
this.openSnackBar('ERROR: failed to create playlist!', '');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
79
src/app/player/player.component.css
Normal file
79
src/app/player/player.component.css
Normal file
@@ -0,0 +1,79 @@
|
||||
.video-player {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.video-player:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.audio-styles {
|
||||
height: 50px;
|
||||
background-color: transparent;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.video-styles {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
::ng-deep .mat-button-toggle-label-content {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.container-video {
|
||||
max-width: 100%;
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
bottom: -1px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
bottom: 3px;
|
||||
left: 3px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
right: 25px;
|
||||
position: absolute;
|
||||
bottom: 25px;
|
||||
}
|
||||
|
||||
.favorite-button {
|
||||
left: 25px;
|
||||
position: absolute;
|
||||
bottom: 25px;
|
||||
}
|
||||
|
||||
.video-col {
|
||||
padding-right: 0px;
|
||||
padding-left: 0.01px;
|
||||
}
|
||||
|
||||
.save-icon {
|
||||
bottom: 1px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.update-playlist-button-div {
|
||||
float: right;
|
||||
margin-right: 30px;
|
||||
margin-top: 25px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.spinner-div {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-right: 12px;
|
||||
top: 8px;
|
||||
}
|
||||
33
src/app/player/player.component.html
Normal file
33
src/app/player/player.component.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<div *ngIf="playlist.length > 0">
|
||||
<div [ngClass]="(type === 'audio') ? null : 'container-video'" class="container">
|
||||
<div style="max-width: 100%; margin-left: 0px;" class="row">
|
||||
<div [ngClass]="(type === 'audio') ? 'my-2 px-1' : 'video-col'" class="col">
|
||||
<vg-player (onPlayerReady)="onPlayerReady($event)" [style.background-color]="(type === 'audio') ? 'transparent' : 'black'">
|
||||
<video [ngClass]="(type === 'audio') ? 'audio-styles' : 'video-styles'" #media class="video-player" [vgMedia]="media" [src]="currentItem.src" id="singleVideo" preload="auto" controls>
|
||||
</video>
|
||||
</vg-player>
|
||||
</div>
|
||||
<div class="col-12 my-2">
|
||||
<mat-button-toggle-group cdkDropList [cdkDropListSortingDisabled]="!id" (cdkDropListDropped)="drop($event)" style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
|
||||
<mat-button-toggle cdkDrag *ngFor="let playlist_item of playlist; let i = index" [checked]="currentItem.title === playlist_item.title" (click)="onClickPlaylistItem(playlist_item, i)" class="toggle-button" [value]="playlist_item.title">{{playlist_item.label}}</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="update-playlist-button-div" *ngIf="id && playlistChanged()">
|
||||
<div class="spinner-div">
|
||||
<mat-spinner *ngIf="playlist_updating" [diameter]="25"></mat-spinner>
|
||||
</div>
|
||||
<button color="primary" [disabled]="playlist_updating" (click)="updatePlaylist()" mat-raised-button>Save changes <mat-icon>update</mat-icon></button>
|
||||
|
||||
</div>
|
||||
|
||||
<div *ngIf="playlist.length > 1">
|
||||
<button class="save-button" color="primary" (click)="downloadContent()" [disabled]="downloading" mat-fab><mat-icon class="save-icon">save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="50"></mat-spinner></button>
|
||||
<button *ngIf="!id" color="accent" class="favorite-button" color="primary" (click)="namePlaylistDialog()" mat-fab><mat-icon class="save-icon">favorite</mat-icon></button>
|
||||
</div>
|
||||
<div *ngIf="playlist.length === 1">
|
||||
<button class="save-button" color="primary" (click)="downloadFile()" [disabled]="downloading" mat-fab><mat-icon class="save-icon">save</mat-icon><mat-spinner *ngIf="downloading" class="spinner" [diameter]="50"></mat-spinner></button>
|
||||
</div>
|
||||
</div>
|
||||
25
src/app/player/player.component.spec.ts
Normal file
25
src/app/player/player.component.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PlayerComponent } from './player.component';
|
||||
|
||||
describe('PlayerComponent', () => {
|
||||
let component: PlayerComponent;
|
||||
let fixture: ComponentFixture<PlayerComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ PlayerComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PlayerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
264
src/app/player/player.component.ts
Normal file
264
src/app/player/player.component.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
import { Component, OnInit, HostListener, EventEmitter } from '@angular/core';
|
||||
import { VgAPI } from 'videogular2/compiled/core';
|
||||
import { PostsService } from 'app/posts.services';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { MatDialog, MatSnackBar } from '@angular/material';
|
||||
import { InputDialogComponent } from 'app/input-dialog/input-dialog.component';
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
|
||||
export interface IMedia {
|
||||
title: string;
|
||||
src: string;
|
||||
type: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-player',
|
||||
templateUrl: './player.component.html',
|
||||
styleUrls: ['./player.component.css']
|
||||
})
|
||||
export class PlayerComponent implements OnInit {
|
||||
|
||||
playlist: Array<IMedia> = [];
|
||||
original_playlist: string = null;
|
||||
playlist_updating = false;
|
||||
|
||||
currentIndex = 0;
|
||||
currentItem: IMedia = null;
|
||||
api: VgAPI;
|
||||
|
||||
// params
|
||||
fileNames: string[];
|
||||
type: string;
|
||||
|
||||
baseStreamPath = null;
|
||||
audioFolderPath = null;
|
||||
videoFolderPath = null;
|
||||
innerWidth: number;
|
||||
|
||||
downloading = false;
|
||||
|
||||
id = null;
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(event) {
|
||||
this.innerWidth = window.innerWidth;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.innerWidth = window.innerWidth;
|
||||
|
||||
this.fileNames = this.route.snapshot.paramMap.get('fileNames').split('|nvr|');
|
||||
this.type = this.route.snapshot.paramMap.get('type');
|
||||
this.id = this.route.snapshot.paramMap.get('id');
|
||||
|
||||
// loading config
|
||||
this.postsService.loadNavItems().subscribe(res => { // loads settings
|
||||
const result = !this.postsService.debugMode ? res['config_file'] : res;
|
||||
this.baseStreamPath = this.postsService.path;
|
||||
this.audioFolderPath = result['YoutubeDLMaterial']['Downloader']['path-audio'];
|
||||
this.videoFolderPath = result['YoutubeDLMaterial']['Downloader']['path-video'];
|
||||
|
||||
|
||||
let fileType = null;
|
||||
if (this.type === 'audio') {
|
||||
fileType = 'audio/mp3';
|
||||
} else if (this.type === 'video') {
|
||||
fileType = 'video/mp4';
|
||||
} else {
|
||||
// error
|
||||
console.error('Must have valid file type! Use \'audio\' or \video\'');
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.fileNames.length; i++) {
|
||||
const fileName = this.fileNames[i];
|
||||
const baseLocation = (this.type === 'audio') ? this.audioFolderPath : this.videoFolderPath;
|
||||
const fullLocation = this.baseStreamPath + baseLocation + encodeURI(fileName); // + (this.type === 'audio' ? '.mp3' : '.mp4');
|
||||
// if it has a slash (meaning it's in a directory), only get the file name for the label
|
||||
let label = null;
|
||||
const decodedName = decodeURIComponent(fileName);
|
||||
const hasSlash = decodedName.includes('/') || decodedName.includes('\\');
|
||||
if (hasSlash) {
|
||||
label = decodedName.replace(/^.*[\\\/]/, '');
|
||||
} else {
|
||||
label = decodedName;
|
||||
}
|
||||
const mediaObject: IMedia = {
|
||||
title: fileName,
|
||||
src: fullLocation,
|
||||
type: fileType,
|
||||
label: label
|
||||
}
|
||||
this.playlist.push(mediaObject);
|
||||
}
|
||||
this.currentItem = this.playlist[this.currentIndex];
|
||||
this.original_playlist = JSON.stringify(this.playlist);
|
||||
});
|
||||
|
||||
// this.getFileInfos();
|
||||
|
||||
}
|
||||
|
||||
constructor(private postsService: PostsService, private route: ActivatedRoute, private dialog: MatDialog, private router: Router,
|
||||
public snackBar: MatSnackBar) {
|
||||
|
||||
}
|
||||
|
||||
onPlayerReady(api: VgAPI) {
|
||||
this.api = api;
|
||||
|
||||
this.api.getDefaultMedia().subscriptions.loadedMetadata.subscribe(this.playVideo.bind(this));
|
||||
this.api.getDefaultMedia().subscriptions.ended.subscribe(this.nextVideo.bind(this));
|
||||
}
|
||||
|
||||
nextVideo() {
|
||||
if (this.currentIndex === this.playlist.length - 1) {
|
||||
// dont continue playing
|
||||
// this.currentIndex = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentIndex++;
|
||||
this.currentItem = this.playlist[ this.currentIndex ];
|
||||
}
|
||||
|
||||
playVideo() {
|
||||
this.api.play();
|
||||
}
|
||||
|
||||
onClickPlaylistItem(item: IMedia, index: number) {
|
||||
// console.log('new current item is ' + item.title + ' at index ' + index);
|
||||
this.currentIndex = index;
|
||||
this.currentItem = item;
|
||||
}
|
||||
|
||||
getFileInfos() {
|
||||
const fileNames = this.getFileNames();
|
||||
this.postsService.getFileInfo(fileNames, this.type, false).subscribe(res => {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
getFileNames() {
|
||||
const fileNames = [];
|
||||
for (let i = 0; i < this.playlist.length; i++) {
|
||||
fileNames.push(this.playlist[i].title);
|
||||
}
|
||||
return fileNames;
|
||||
}
|
||||
|
||||
decodeURI(string) {
|
||||
return decodeURI(string);
|
||||
}
|
||||
|
||||
downloadContent() {
|
||||
const fileNames = [];
|
||||
for (let i = 0; i < this.playlist.length; i++) {
|
||||
fileNames.push(this.playlist[i].title);
|
||||
}
|
||||
|
||||
const zipName = fileNames[0].split(' ')[0] + fileNames[1].split(' ')[0];
|
||||
this.downloading = true;
|
||||
this.postsService.downloadFileFromServer(fileNames, this.type, zipName).subscribe(res => {
|
||||
this.downloading = false;
|
||||
const blob: Blob = res;
|
||||
saveAs(blob, zipName + '.zip');
|
||||
}, err => {
|
||||
console.log(err);
|
||||
this.downloading = false;
|
||||
});
|
||||
}
|
||||
|
||||
downloadFile() {
|
||||
const ext = (this.type === 'audio') ? '.mp3' : '.mp4';
|
||||
const filename = this.playlist[0].title;
|
||||
this.downloading = true;
|
||||
this.postsService.downloadFileFromServer(filename, this.type).subscribe(res => {
|
||||
this.downloading = false;
|
||||
const blob: Blob = res;
|
||||
saveAs(blob, filename + ext);
|
||||
}, err => {
|
||||
console.log(err);
|
||||
this.downloading = false;
|
||||
});
|
||||
}
|
||||
|
||||
namePlaylistDialog() {
|
||||
const done = new EventEmitter<any>();
|
||||
const dialogRef = this.dialog.open(InputDialogComponent, {
|
||||
width: '300px',
|
||||
data: {
|
||||
inputTitle: 'Name the playlist',
|
||||
inputPlaceholder: 'Name',
|
||||
submitText: 'Favorite',
|
||||
doneEmitter: done
|
||||
}
|
||||
});
|
||||
|
||||
done.subscribe(name => {
|
||||
|
||||
// Eventually do additional checks on name
|
||||
if (name) {
|
||||
const fileNames = this.getFileNames();
|
||||
this.postsService.createPlaylist(name, fileNames, this.type, null).subscribe(res => {
|
||||
if (res['success']) {
|
||||
dialogRef.close();
|
||||
const new_playlist = res['new_playlist'];
|
||||
this.openSnackBar('Playlist \'' + name + '\' successfully created!', '')
|
||||
this.playlistPostCreationHandler(new_playlist.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
createPlaylist(name) {
|
||||
this.postsService.createPlaylist(name, this.fileNames, this.type, null).subscribe(res => {
|
||||
if (res['success']) {
|
||||
console.log('Success!');
|
||||
}
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
playlistPostCreationHandler(playlistID) {
|
||||
// changes the route without moving from the current view or
|
||||
// triggering a navigation event
|
||||
this.id = playlistID;
|
||||
this.router.navigateByUrl(this.router.url + ';id=' + playlistID);
|
||||
}
|
||||
|
||||
drop(event: CdkDragDrop<string[]>) {
|
||||
moveItemInArray(this.playlist, event.previousIndex, event.currentIndex);
|
||||
}
|
||||
|
||||
playlistChanged() {
|
||||
return JSON.stringify(this.playlist) !== this.original_playlist;
|
||||
}
|
||||
|
||||
updatePlaylist() {
|
||||
const fileNames = this.getFileNames();
|
||||
this.playlist_updating = true;
|
||||
this.postsService.updatePlaylist(this.id, fileNames, this.type).subscribe(res => {
|
||||
this.playlist_updating = false;
|
||||
if (res['success']) {
|
||||
const fileNamesEncoded = fileNames.join('|nvr|');
|
||||
this.router.navigate(['/player', {fileNames: fileNamesEncoded, type: 'video', id: this.id}]);
|
||||
this.openSnackBar('Successfully updated playlist.', '');
|
||||
this.original_playlist = JSON.stringify(this.playlist);
|
||||
} else {
|
||||
this.openSnackBar('ERROR: Failed to update playlist.', '');
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// snackbar helper
|
||||
public openSnackBar(message: string, action: string) {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,74 +1,140 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Http} from '@angular/http';
|
||||
import {Injectable, isDevMode, Inject} from '@angular/core';
|
||||
import { HttpClient, HttpHeaders, HttpRequest, HttpResponseBase } from '@angular/common/http';
|
||||
import config from '../assets/default.json';
|
||||
import 'rxjs/add/operator/map';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/operator/catch';
|
||||
import 'rxjs/add/observable/throw';
|
||||
import { THEMES_CONFIG } from '../themes';
|
||||
import { Router } from '@angular/router';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
|
||||
@Injectable()
|
||||
export class PostsService {
|
||||
path: string = "";
|
||||
audioFolder: string = "";
|
||||
videoFolder: string = "";
|
||||
startPath: string = "http://localhost:17442/";
|
||||
startPathSSL: string = "https://localhost:17442/"
|
||||
handShakeComplete: boolean = false;
|
||||
path = '';
|
||||
audioFolder = '';
|
||||
videoFolder = '';
|
||||
startPath = null; // 'http://localhost:17442/';
|
||||
startPathSSL = null; // 'https://localhost:17442/'
|
||||
handShakeComplete = false;
|
||||
THEMES_CONFIG = THEMES_CONFIG;
|
||||
theme;
|
||||
|
||||
constructor(private http: Http){
|
||||
debugMode = false;
|
||||
constructor(private http: HttpClient, private router: Router, @Inject(DOCUMENT) private document: Document) {
|
||||
console.log('PostsService Initialized...');
|
||||
// this.startPath = window.location.href + '/api/';
|
||||
// this.startPathSSL = window.location.href + '/api/';
|
||||
this.path = this.document.location.origin + '/api/';
|
||||
|
||||
if (isDevMode()) {
|
||||
this.debugMode = true;
|
||||
this.path = 'http://localhost:17442/api/';
|
||||
}
|
||||
}
|
||||
|
||||
startHandshake(url: string): Observable<string>
|
||||
{
|
||||
return this.http.get(url + "geturl")
|
||||
.map(res => res.json());
|
||||
setTheme(theme) {
|
||||
this.theme = this.THEMES_CONFIG[theme];
|
||||
}
|
||||
|
||||
startHandshakeSSL(url: string): Observable<string>
|
||||
{
|
||||
return this.http.get(url + "geturl")
|
||||
.map(res => res.json());
|
||||
startHandshake(url: string) {
|
||||
return this.http.get(url + 'geturl');
|
||||
}
|
||||
|
||||
getVideoFolder(): Observable<string>
|
||||
{
|
||||
return this.http.get(this.startPath + "videofolder")
|
||||
.map(res => res.json());
|
||||
startHandshakeSSL(url: string) {
|
||||
return this.http.get(url + 'geturl');
|
||||
}
|
||||
|
||||
getAudioFolder(): Observable<string>
|
||||
{
|
||||
return this.http.get(this.startPath + "audiofolder")
|
||||
.map(res => res.json());
|
||||
getVideoFolder() {
|
||||
return this.http.get(this.startPath + 'videofolder');
|
||||
}
|
||||
|
||||
makeMP3(url: string): Observable<string>
|
||||
{
|
||||
return this.http.post(this.path + "tomp3",{url: url})
|
||||
.map(res => res.json());
|
||||
getAudioFolder() {
|
||||
return this.http.get(this.startPath + 'audiofolder');
|
||||
}
|
||||
|
||||
makeMP4(url: string): Observable<string>
|
||||
{
|
||||
return this.http.post(this.path + "tomp4",{url: url})
|
||||
.map(res => res.json());
|
||||
// tslint:disable-next-line: max-line-length
|
||||
makeMP3(url: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, customOutput: string = null, youtubeUsername: string = null, youtubePassword: string = null) {
|
||||
return this.http.post(this.path + 'tomp3', {url: url,
|
||||
maxBitrate: selectedQuality,
|
||||
customQualityConfiguration: customQualityConfiguration,
|
||||
customArgs: customArgs,
|
||||
customOutput: customOutput,
|
||||
youtubeUsername: youtubeUsername,
|
||||
youtubePassword: youtubePassword});
|
||||
}
|
||||
|
||||
getFileStatusMp3(name: string): Observable<any> {
|
||||
return this.http.post(this.path + "mp3fileexists",{name: name})
|
||||
.map(res => res.json());
|
||||
// tslint:disable-next-line: max-line-length
|
||||
makeMP4(url: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, customOutput: string = null, youtubeUsername: string = null, youtubePassword: string = null) {
|
||||
return this.http.post(this.path + 'tomp4', {url: url,
|
||||
selectedHeight: selectedQuality,
|
||||
customQualityConfiguration: customQualityConfiguration,
|
||||
customArgs: customArgs,
|
||||
customOutput: customOutput,
|
||||
youtubeUsername: youtubeUsername,
|
||||
youtubePassword: youtubePassword});
|
||||
}
|
||||
|
||||
getFileStatusMp4(name: string): Observable<any> {
|
||||
return this.http.post(this.path + "mp4fileexists",{name: name})
|
||||
.map(res => res.json());
|
||||
getFileStatusMp3(name: string) {
|
||||
return this.http.post(this.path + 'fileStatusMp3', {name: name});
|
||||
}
|
||||
|
||||
getFileStatusMp4(name: string) {
|
||||
return this.http.post(this.path + 'fileStatusMp4', {name: name});
|
||||
}
|
||||
|
||||
loadNavItems() {
|
||||
console.log("Config location: " + window.location.href + "backend/config/default.json");
|
||||
return this.http.get(window.location.href + "backend/config/default.json")
|
||||
.map(res => res.json());
|
||||
if (isDevMode()) {
|
||||
return this.http.get('./assets/default.json');
|
||||
} else {
|
||||
return this.http.get(this.path + 'config');
|
||||
}
|
||||
}
|
||||
|
||||
deleteFile(name: string, isAudio: boolean) {
|
||||
if (isAudio) {
|
||||
return this.http.post(this.path + 'deleteMp3', {name: name});
|
||||
} else {
|
||||
return this.http.post(this.path + 'deleteMp4', {name: name});
|
||||
}
|
||||
}
|
||||
|
||||
getMp3s() {
|
||||
return this.http.post(this.path + 'getMp3s', {});
|
||||
}
|
||||
|
||||
getMp4s() {
|
||||
return this.http.post(this.path + 'getMp4s', {});
|
||||
}
|
||||
|
||||
downloadFileFromServer(fileName, type, outputName = null) {
|
||||
return this.http.post(this.path + 'downloadFile', {fileNames: fileName,
|
||||
type: type,
|
||||
is_playlist: Array.isArray(fileName),
|
||||
outputName: outputName},
|
||||
{responseType: 'blob'});
|
||||
}
|
||||
|
||||
getFileInfo(fileNames, type, urlMode) {
|
||||
return this.http.post(this.path + 'getVideoInfos', {fileNames: fileNames, type: type, urlMode: urlMode});
|
||||
}
|
||||
|
||||
createPlaylist(playlistName, fileNames, type, thumbnailURL) {
|
||||
return this.http.post(this.path + 'createPlaylist', {playlistName: playlistName,
|
||||
fileNames: fileNames,
|
||||
type: type,
|
||||
thumbnailURL: thumbnailURL});
|
||||
}
|
||||
|
||||
updatePlaylist(playlistID, fileNames, type) {
|
||||
return this.http.post(this.path + 'updatePlaylist', {playlistID: playlistID,
|
||||
fileNames: fileNames,
|
||||
type: type});
|
||||
}
|
||||
|
||||
removePlaylist(playlistID, type) {
|
||||
return this.http.post(this.path + 'deletePlaylist', {playlistID: playlistID, type: type});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
12
src/app/youtube-search.service.spec.ts
Normal file
12
src/app/youtube-search.service.spec.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { YoutubeSearchService } from './youtube-search.service';
|
||||
|
||||
describe('YoutubeSearchService', () => {
|
||||
beforeEach(() => TestBed.configureTestingModule({}));
|
||||
|
||||
it('should be created', () => {
|
||||
const service: YoutubeSearchService = TestBed.get(YoutubeSearchService);
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
101
src/app/youtube-search.service.ts
Normal file
101
src/app/youtube-search.service.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export class Result {
|
||||
id: string
|
||||
title: string
|
||||
desc: string
|
||||
thumbnailUrl: string
|
||||
videoUrl: string
|
||||
uploaded: any;
|
||||
|
||||
constructor(obj?: any) {
|
||||
this.id = obj && obj.id || null
|
||||
this.title = obj && obj.title || null
|
||||
this.desc = obj && obj.desc || null
|
||||
this.thumbnailUrl = obj && obj.thumbnailUrl || null
|
||||
this.uploaded = obj && obj.uploaded || null
|
||||
this.videoUrl = obj && obj.videoUrl || `https://www.youtube.com/watch?v=${this.id}`
|
||||
|
||||
this.uploaded = formatDate(Date.parse(this.uploaded));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class YoutubeSearchService {
|
||||
|
||||
url = 'https://www.googleapis.com/youtube/v3/search';
|
||||
key = null;
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
initializeAPI(key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
search(query: string): Observable<Result[]> {
|
||||
if (this.ValidURL(query)) {
|
||||
return new Observable<Result[]>();
|
||||
}
|
||||
const params: string = [
|
||||
`q=${query}`,
|
||||
`key=${this.key}`,
|
||||
`part=snippet`,
|
||||
`type=video`,
|
||||
`maxResults=5`
|
||||
].join('&')
|
||||
const queryUrl = `${this.url}?${params}`
|
||||
return this.http.get(queryUrl).map(response => {
|
||||
return <any>response['items'].map(item => {
|
||||
return new Result({
|
||||
id: item.id.videoId,
|
||||
title: item.snippet.title,
|
||||
desc: item.snippet.description,
|
||||
thumbnailUrl: item.snippet.thumbnails.high.url,
|
||||
uploaded: item.snippet.publishedAt
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// checks if url is a valid URL
|
||||
ValidURL(str) {
|
||||
// tslint:disable-next-line: max-line-length
|
||||
const strRegex = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/;
|
||||
const re = new RegExp(strRegex);
|
||||
return re.test(str);
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateVal) {
|
||||
const newDate = new Date(dateVal);
|
||||
|
||||
const sMonth = padValue(newDate.getMonth() + 1);
|
||||
const sDay = padValue(newDate.getDate());
|
||||
const sYear = newDate.getFullYear();
|
||||
let sHour: any;
|
||||
sHour = newDate.getHours();
|
||||
const sMinute = padValue(newDate.getMinutes());
|
||||
let sAMPM = 'AM';
|
||||
|
||||
const iHourCheck = parseInt(sHour, 10);
|
||||
|
||||
if (iHourCheck > 12) {
|
||||
sAMPM = 'PM';
|
||||
sHour = iHourCheck - 12;
|
||||
} else if (iHourCheck === 0) {
|
||||
sHour = '12';
|
||||
}
|
||||
|
||||
sHour = padValue(sHour);
|
||||
|
||||
return sMonth + '-' + sDay + '-' + sYear + ' ' + sHour + ':' + sMinute + ' ' + sAMPM;
|
||||
}
|
||||
|
||||
function padValue(value) {
|
||||
return (value < 10) ? '0' + value : value;
|
||||
}
|
||||
37
src/assets/default.json
Normal file
37
src/assets/default.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"YoutubeDLMaterial": {
|
||||
"Host": {
|
||||
"url": "http://localhost",
|
||||
"port": "17442"
|
||||
},
|
||||
"Encryption": {
|
||||
"use-encryption": false,
|
||||
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
|
||||
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
|
||||
},
|
||||
"Downloader": {
|
||||
"path-audio": "audio/",
|
||||
"path-video": "video/"
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "Youtube Downloader",
|
||||
"file_manager_enabled": true,
|
||||
"allow_quality_select": true,
|
||||
"download_only_mode": false,
|
||||
"allow_multi_download_mode": true
|
||||
},
|
||||
"API": {
|
||||
"use_youtube_API": false,
|
||||
"youtube_API_key": ""
|
||||
},
|
||||
"Themes": {
|
||||
"default_theme": "default",
|
||||
"allow_theme_change": true
|
||||
},
|
||||
"Advanced": {
|
||||
"use_default_downloading_agent": true,
|
||||
"custom_downloading_agent": "",
|
||||
"allow_advanced_download": true
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
src/favicon.ico
BIN
src/favicon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 21 KiB |
@@ -7,11 +7,10 @@
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
|
||||
<link href="https://unpkg.com/@angular/material/prebuilt-themes/indigo-pink.css" rel="stylesheet"> <script src="systemjs.config.js"></script>
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined" rel="stylesheet"/>
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
|
||||
@@ -42,12 +42,11 @@
|
||||
|
||||
|
||||
/** Evergreen browsers require these. **/
|
||||
import 'core-js/es6/reflect';
|
||||
import 'core-js/es7/reflect';
|
||||
// import 'core-js/es6/reflect';
|
||||
|
||||
|
||||
/** ALL Firefox browsers require the following to support `@angular/animation`. **/
|
||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
70
src/styles.scss
Normal file
70
src/styles.scss
Normal file
@@ -0,0 +1,70 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
|
||||
@import '@angular/material/prebuilt-themes/indigo-pink.css';
|
||||
|
||||
//@import './app-theme';
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
// @import "../node_modules/@angular/material/prebuilt-themes/purple-green.css";
|
||||
@import "palette.scss";
|
||||
|
||||
html, body { height: 100%; }
|
||||
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
|
||||
|
||||
@import '~@angular/material/theming';
|
||||
// Plus imports for other components in your app.
|
||||
|
||||
/*// Typography
|
||||
$custom-typography: mat-typography-config(
|
||||
$font-family: Raleway,
|
||||
$headline: mat-typography-level(24px, 48px, 400),
|
||||
$body-1: mat-typography-level(16px, 24px, 400)
|
||||
);
|
||||
@include angular-material-typography($custom-typography);
|
||||
*/
|
||||
// Default colors
|
||||
$my-app-primary: mat-palette($mat-light-blue, 700, 100, 800);
|
||||
$my-app-accent: mat-palette($mat-blue, 700, 100, 800);
|
||||
$my-app-warn: mat-palette($mat-red, 700, 100, 800);
|
||||
|
||||
$my-app-theme: mat-light-theme($my-app-primary, $my-app-accent, $my-app-warn);
|
||||
@include angular-material-theme($my-app-theme);
|
||||
|
||||
// Dark theme
|
||||
$dark-primary: mat-palette($mat-indigo);
|
||||
$dark-accent: mat-palette($mat-blue);
|
||||
$dark-warn: mat-palette($mat-deep-orange);
|
||||
|
||||
$dark-theme: mat-dark-theme($dark-primary, $dark-accent, $dark-warn);
|
||||
|
||||
.dark-theme {
|
||||
@include angular-material-theme($dark-theme);
|
||||
}
|
||||
|
||||
// Light theme
|
||||
$light-primary: mat-palette($mat-grey, 200, 500, 300);
|
||||
$light-accent: mat-palette($mat-brown, 200);
|
||||
$light-warn: mat-palette($mat-deep-orange, 200);
|
||||
|
||||
$light-theme: mat-light-theme($light-primary, $light-accent, $light-warn);
|
||||
|
||||
.light-theme {
|
||||
@include angular-material-theme($light-theme)
|
||||
}
|
||||
|
||||
.no-outline {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
// Include the common styles for Angular Material. We include this here so that you only
|
||||
// have to load a single css file for Angular Material in your app.
|
||||
// Be sure that you only ever include this mixin once!
|
||||
@include mat-core();
|
||||
|
||||
// @import '../node_modules/@angular/material/theming';
|
||||
|
||||
.centered {
|
||||
margin: 0 auto;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
}
|
||||
22
src/themes.ts
Normal file
22
src/themes.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
const THEMES_CONFIG = {
|
||||
'default': {
|
||||
'key': 'default',
|
||||
'background_color': 'ghostwhite',
|
||||
'css_label': 'default-theme',
|
||||
'social_theme': 'material-light'
|
||||
},
|
||||
'dark': {
|
||||
'key': 'dark',
|
||||
'background_color': '#757575',
|
||||
'css_label': 'dark-theme',
|
||||
'social_theme': 'material-dark'
|
||||
},
|
||||
'light': {
|
||||
'key': 'light',
|
||||
'background_color': 'white',
|
||||
'css_label': 'light-theme',
|
||||
'social_theme': 'material-light'
|
||||
}
|
||||
};
|
||||
|
||||
export {THEMES_CONFIG};
|
||||
@@ -2,7 +2,6 @@
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/app",
|
||||
"module": "es2015",
|
||||
"baseUrl": "",
|
||||
"types": []
|
||||
},
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/spec",
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"baseUrl": "",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"test.ts"
|
||||
],
|
||||
"include": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.d.ts"
|
||||
]
|
||||
}
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/spec",
|
||||
"baseUrl": "",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"test.ts",
|
||||
"polyfills.ts"
|
||||
|
||||
],
|
||||
"include": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.d.ts"
|
||||
]
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"downlevelIteration": true,
|
||||
"importHelpers": true,
|
||||
"outDir": "./dist/out-tsc",
|
||||
"baseUrl": "src",
|
||||
"sourceMap": true,
|
||||
@@ -8,13 +10,16 @@
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "es5",
|
||||
"target": "es2015",
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
],
|
||||
"lib": [
|
||||
"es2016",
|
||||
"dom"
|
||||
]
|
||||
],
|
||||
"module": "esnext"
|
||||
}
|
||||
}
|
||||
}
|
||||
11
tslint.json
11
tslint.json
@@ -14,8 +14,7 @@
|
||||
"eofline": true,
|
||||
"forin": true,
|
||||
"import-blacklist": [
|
||||
true,
|
||||
"rxjs"
|
||||
true
|
||||
],
|
||||
"import-spacing": true,
|
||||
"indent": [
|
||||
@@ -119,12 +118,12 @@
|
||||
"app",
|
||||
"kebab-case"
|
||||
],
|
||||
"use-input-property-decorator": true,
|
||||
"use-output-property-decorator": true,
|
||||
"use-host-property-decorator": true,
|
||||
"no-inputs-metadata-property": true,
|
||||
"no-outputs-metadata-property": true,
|
||||
"no-host-metadata-property": true,
|
||||
"no-input-rename": true,
|
||||
"no-output-rename": true,
|
||||
"use-life-cycle-interface": true,
|
||||
"use-lifecycle-interface": true,
|
||||
"use-pipe-transform-interface": true,
|
||||
"component-class-suffix": true,
|
||||
"directive-class-suffix": true,
|
||||
|
||||
Reference in New Issue
Block a user