Compare commits

...

54 Commits
v1.0 ... v3.0

Author SHA1 Message Date
Isaac Grynsztein
393ed5a210 added skeleton code for future electron.js support
added font swap to google font call

simplified polyfills

updated backend package.json info
2020-02-20 18:35:09 -05:00
Isaac Grynsztein
54492b109a thumbnails now lazy load. when it is loading, a content loading gradient is shown in front of it
made file cards look better on mobile devices
2020-02-20 15:45:40 -05:00
Isaac Grynsztein
7eac88a31f removed debug logging 2020-02-20 15:44:44 -05:00
Isaac Grynsztein
8fec9639eb fixed bug where if no theme was selected, errors would fire 2020-02-20 14:30:05 -05:00
Isaac Grynsztein
a15e1f98fa fixed compilation error and cleaned up code in app component 2020-02-20 14:29:29 -05:00
Isaac Grynsztein
6604484765 updated styles.css to styles.scss in angular.json 2020-02-20 10:45:59 -05:00
Isaac Grynsztein
c58f8a4058 added theming support with 3 themes (only 2 selectable for now)
switched from css to scss default style system

cleaned up unused code in app component

upated youtube search results styling

downloading video from home screen now shows local progress bar under that video
2020-02-20 10:45:37 -05:00
Isaac Grynsztein
8545016f1d "audio only" checkbox is now remembered after page loads
removed videogular icons as it caused compilation errors
2020-02-19 02:45:05 -05:00
Isaac Grynsztein
9b1e84821e moved theme to internal file 2020-02-19 02:43:58 -05:00
Isaac Grynsztein
6505fad7bc added save button to player component and updated download button 2020-02-19 02:29:36 -05:00
Isaac Grynsztein
d245904c0d added the ability to save playlists
added local db system (lowdb)

playlists are now downloaded as a zip from the streaming menu
2020-02-19 02:29:10 -05:00
Isaac Grynsztein
0095ea1271 fixed bug where search results showed old results when search bar was empty 2020-02-18 18:00:39 -05:00
Isaac Grynsztein
b41d10f514 Added download button to player component 2020-02-18 17:29:34 -05:00
Isaac Grynsztein
8e3d6a0af6 Player compilation error fixed
removed debug statements in player component
2020-02-17 17:42:50 -05:00
Isaac Grynsztein
1e4995c5ce Fixed catch statements not having arguments on backend
Fixed backend location url not working when not in root dir on web server
2020-02-17 17:42:21 -05:00
Isaac Grynsztein
710e3613a8 removed debug statements 2020-02-17 00:40:23 -05:00
Isaac Grynsztein
28331c1037 Updated debug-only default.json to reflect the new options added 2020-02-17 00:37:41 -05:00
Isaac Grynsztein
202c0718b7 Updated debug launch.json to include frontend debugging 2020-02-17 00:37:06 -05:00
Isaac Grynsztein
a985963661 Adding updated bootstrap to index.html 2020-02-17 00:36:33 -05:00
Isaac Grynsztein
f673b325fd Added custom quality options to PostsService and the ability to do a URL info grab from the server
Video and audio streams now save the stream object in a "descriptors" variable which will give the server the ability to close them when the file needs to be deleted.
- without this, windows systems don't play nice with nodejs function fs.unlinkSync. A weird, but necessary workaround

deleting files is now done asynchronously, and success is now determined by whether they exist afterwards or not

Added backend function to get info for URLs

Modified tomp3 and tomp4 endpoint to support custom quality settings.
2020-02-17 00:36:15 -05:00
Isaac Grynsztein
5f4a7a1e69 Added support for custom quality settings for video and audio files.
Available formats are downloaded when a valid YT url is detected. These formats are then parsed and a best audio format is selected based on the results

After downloading a file with no file manager, file is now deleted. After file deletion, mp3/mp4 reload occurs

Updated view on main component to be more responsive, using bootstrap grid

Updated progress bar UI-wise to be more in line with the rest of the page
2020-02-17 00:32:31 -05:00
Isaac Grynsztein
d54b2a73c8 Updated view of player to be more responsive. Window width is now recorded for eventual use for responsiveness optimization 2020-02-17 00:28:49 -05:00
Isaac Grynsztein
8e7bb4ba3b added custom player
added routing with two routes: home and player

moved most of app component to main component. app component currently just manages the top toolbar
2020-02-15 02:13:21 -05:00
Isaac Grynsztein
d595de5786 added functions to get info on a downloaded (or downloading) file
bug fixed where videos with quotations would not properly stream
2020-02-15 02:11:21 -05:00
Isaac Grynsztein
31394fa98c updated mobile view for file cards to be more responsive
streamed audio/video now include the extension in the download

cleaned up unused code in app component
2020-02-14 00:17:51 -05:00
Isaac Grynsztein
af595d3df8 Added debug mode to server and relevant debug configurations
simplified youtubedl download process to speed up the download

queryurl not printed any longer by youtube search service
2020-02-14 00:15:55 -05:00
Isaac Grynsztein
81377a2b38 added youtube search functionality in frontend 2020-02-13 06:43:49 -05:00
Isaac Grynsztein
35bdd1deeb fixed file name paths on backend. backend also now tells frontend when the url provided is a playlist
frontend now does not get the file status and simply waits for the server to respond with the file

added methods to download audio/video files to simplify downloadHelperMp3/Mp4
2020-02-13 05:10:27 -05:00
Isaac Grynsztein
a1ec53edb9 preparing config for youtube search feature 2020-02-13 05:08:14 -05:00
Isaac Grynsztein
aa130d3fc9 updated youtube-dl version for nodejs 2020-02-13 03:16:34 -05:00
Isaac Grynsztein
0f0bf3a401 updated youtube-dl.exe binary for windows 2020-02-13 03:13:20 -05:00
Isaac Grynsztein
73b9c61080 renamed variable in backend
deleteaudiofile/deletevideofile functions now made for reusability

downloaded videos now use the title as the file name. this requires longer download times as 2 calls are created

created a deletefile http call in backend, however it is currently not being used
2020-02-13 03:13:10 -05:00
Isaac Grynsztein
501806909a fixed bug where going back to the page after entering a stream didn't allow downloading of new files
in download only mode, files are now auto deleted when saved
2020-02-13 03:10:52 -05:00
Isaac Grynsztein
77dd96b3b9 added download_only_mode to encryption configuration
reloading of mp3s/mp4s only happens if file manager is enabled
2020-02-11 16:36:59 -05:00
Isaac Grynsztein
505b145bb3 commented out debug console messages 2020-02-11 16:28:14 -05:00
Isaac Grynsztein
ba5592015d added download only mode that simply downloads files to the client when the server finishes the download
added dependency on savefile library for download-only mode
2020-02-11 13:10:02 -05:00
Isaac Grynsztein
10c90a01f2 linted files 2020-02-09 23:35:01 -05:00
Isaac Grynsztein
abaa799628 quick dependency fix 2019-10-21 02:51:13 -04:00
Isaac Grynsztein
9e148d1464 more stylistic improvements 2019-10-21 02:46:22 -04:00
Isaac Grynsztein
9fa646132d added stylistic spinner 2019-10-21 02:25:03 -04:00
Isaac Grynsztein
f98ba00551 fixed bug preventing app being used outside root url 2019-10-21 02:24:47 -04:00
Isaac Grynsztein
bc1e0ea542 minor change 2019-10-20 19:16:10 -04:00
Isaac Grynsztein
9ebb684d5c Updated backend to reflect frontend changes 2019-10-20 19:15:26 -04:00
Isaac Grynsztein
91713f1140 Migrated to angular 8
Created dev configuration
2019-10-20 19:15:13 -04:00
Isaac Grynsztein
377676cda4 Stylistic improvements and moved away from @angular/http 2019-10-20 19:14:01 -04:00
Isaac Grynsztein
8e445bb80d Made repository changes 2019-10-20 17:39:02 -04:00
Tzahi12345
f0bde1efc4 Update README.md 2018-01-23 00:38:20 -05:00
Isaac Grynsztein
4be6f341da Updated configuration 2018-01-23 00:17:20 -05:00
Isaac Grynsztein
dd62f1a7b3 Added configuration option for file manager 2018-01-22 23:49:13 -05:00
Isaac Grynsztein
1cdd4d0e15 Added support for modifying downloaded files 2018-01-22 03:43:47 -05:00
Tzahi12345
e4e1e67855 Update README.md 2018-01-20 22:13:47 -05:00
Tzahi12345
867d2394de Update README.md 2018-01-20 22:12:06 -05:00
Tzahi12345
eb54c2fda5 Create LICENSE.md 2018-01-20 20:26:37 -05:00
Tzahi12345
deacf5a49a Update README.md 2018-01-20 20:23:39 -05:00
52 changed files with 3118 additions and 513 deletions

View File

@@ -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": {}
}
}

3
.gitignore vendored
View File

@@ -45,4 +45,5 @@ node_modules/*
backend/node_modules/*
YoutubeDL-Material/node_modules/*
backend/video/*
backend/audio/*
backend/audio/*
src/assets/default.json

21
.vscode/launch.json vendored Normal file
View 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}"
}
]
}

7
LICENSE.md Normal file
View 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.

View File

@@ -1,28 +1,66 @@
# 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 5](https://angular.io/) for the frontend, and [Nodejs](https://nodejs.org/) on the backend.
## Development server
## Getting Started
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.
Check out the prerequisites, and go to the installation section. Easy as pie!
## Code scaffolding
Here's an image of what it'll look like once you're done:
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|module`.
![frontpage](https://i.imgur.com/m3xozES.png)
## Build
With optional file management enabled (default):
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.
![frontpage_with_files](https://i.imgur.com/z9vME2O.png)
## Running unit tests
### Prerequisites
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
You need to have a functioning web server for this to work. Also make sure you have these dependencies installed on your system: ffmpeg, nodejs, python. If you don't, run this command:
## Running end-to-end tests
```
sudo apt-get install ffmpeg nodejs python
```
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`.
### Installing
## Further help
First, download the [latest release](https://github.com/Tzahi12345/YoutubeDL-Material/releases/latest)!
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).
Drag all the files in `youtubedl-material` to a location accessible to a web server. It works best if it's the root (usually right inside `public_html`. Once that's done, navigate to `backend` and edit the `default.json` file. If you're using SSL encryption, look at the `encrypted.json` file for a template.
Port forward `17442` if you're going to access YoutubeDL-Material from out of your network. This is an important step. Make sure the configuration reflects this appropriately.
Once the configuration is done, type `sudo nodejs app.js`. This will run the backend server. On your browser, navigate to your installation folder. 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.
## 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 a web server, and drag the `backend` directory into the same folder. This folder should have `index.html` in it as well. If it does **not**, you're in the wrong directory.
The frontend is now complete. The backend is much easier. Just go into the `youtubedl-material/backend` folder, and type `sudo nodejs app.js`.
Finally, port forward the port `17442` and point it to the server's IP address. Make sure the port is also allowed through the firewall.
## 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
View 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"
}
}
}

View File

@@ -6,16 +6,40 @@ var config = require('config');
var https = require('https');
var express = require("express");
var bodyParser = require("body-parser");
var pem = require('https-pem');
var archiver = require('archiver');
const low = require('lowdb')
var URL = require('url').URL;
const shortid = require('shortid')
var app = express();
var frontendUrl = config.get("YoutubeDLMaterial.Host.frontendurl");
const FileSync = require('lowdb/adapters/FileSync')
const adapter = new FileSync('db.json');
const db = low(adapter)
// Set some defaults
db.defaults({ playlists: {
audio: [],
video: []
}}).write();
// check if debug mode
let debugMode = process.env.YTDL_MODE === 'debug';
if (debugMode) console.log('YTDL-Material in debug mode!');
var frontendUrl = !debugMode ? config.get("YoutubeDLMaterial.Host.frontendurl") : 'http://localhost:4200';
var backendUrl = config.get("YoutubeDLMaterial.Host.backendurl")
var backendPort = 17442;
var usingEncryption = config.get("YoutubeDLMaterial.Encryption.use-encryption");
var basePath = config.get("YoutubeDLMaterial.Downloader.path-base");
var audioPath = config.get("YoutubeDLMaterial.Downloader.path-audio");
var videoPath = config.get("YoutubeDLMaterial.Downloader.path-video");
var audioFolderPath = config.get("YoutubeDLMaterial.Downloader.path-audio");
var videoFolderPath = config.get("YoutubeDLMaterial.Downloader.path-video");
var downloadOnlyMode = config.get("YoutubeDLMaterial.Extra.download_only_mode")
var descriptors = {};
if (usingEncryption)
{
@@ -37,8 +61,10 @@ if (usingEncryption)
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
var url_domain = new URL(frontendUrl);
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", frontendUrl);
res.header("Access-Control-Allow-Origin", url_domain.origin);
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
@@ -48,42 +74,35 @@ app.get('/using-encryption', function(req, res) {
res.end("yes");
});
// objects
app.post('/tomp3', function(req, res) {
var url = req.body.url;
var date = Date.now();
var path = audioPath;
var audiopath = Date.now();
youtubedl.exec(url, ['-o', path + audiopath + ".mp3", '-x', '--audio-format', 'mp3', '--write-info-json'], {}, function(err, output) {
if (err) {
audiopath = "-1";
throw err;
}
});
function File(id, title, thumbnailURL, isAudio, duration) {
this.id = id;
this.title = title;
this.thumbnailURL = thumbnailURL;
this.isAudio = isAudio;
this.duration = duration;
}
// write file info
// actual functions
youtubedl.getInfo(url, function(err, info) {
if (err) throw err;
var size = info.size;
fs.writeFile("data/"+audiopath, size, function(err) {
if(err) {
return console.log(err);
}
console.log("The file was saved!");
});
});
var completeString = "done";
var audiopathEncoded = encodeURIComponent(audiopath);
res.send(audiopathEncoded);
res.end("yes");
});
function getThumbnailMp3(name)
{
var obj = getJSONMp3(name);
var thumbnailLink = obj.thumbnail;
return thumbnailLink;
}
function getThumbnailMp4(name)
{
var obj = getJSONMp4(name);
var thumbnailLink = obj.thumbnail;
return thumbnailLink;
}
function getFileSizeMp3(name)
{
var jsonPath = audioPath+name+".mp3.info.json";
var jsonPath = audioFolderPath+name+".mp3.info.json";
if (fs.existsSync(jsonPath))
var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
@@ -93,22 +112,9 @@ function getFileSizeMp3(name)
return obj.filesize;
}
function getAmountDownloadedMp3(name)
{
var partPath = audioPath+name+".mp3.part";
if (fs.existsSync(partPath))
{
const stats = fs.statSync(partPath);
const fileSizeInBytes = stats.size;
return fileSizeInBytes;
}
else
return 0;
}
function getFileSizeMp4(name)
{
var jsonPath = videoPath+name+".info.json";
var jsonPath = videoFolderPath+name+".info.json";
var filesize = 0;
if (fs.existsSync(jsonPath))
{
@@ -126,10 +132,47 @@ function getFileSizeMp4(name)
return filesize;
}
function getJSONMp3(name)
{
var jsonPath = audioFolderPath+name+".mp3.info.json";
if (fs.existsSync(jsonPath))
var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
else
var obj = 0;
return obj;
}
function getJSONMp4(name)
{
var jsonPath = videoFolderPath+name+".info.json";
if (fs.existsSync(jsonPath))
{
var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
return obj;
}
else return 0;
}
function getAmountDownloadedMp3(name)
{
var partPath = audioFolderPath+name+".mp3.part";
if (fs.existsSync(partPath))
{
const stats = fs.statSync(partPath);
const fileSizeInBytes = stats.size;
return fileSizeInBytes;
}
else
return 0;
}
function getAmountDownloadedMp4(name)
{
var format = getVideoFormatID(name);
var partPath = videoPath+name+".f"+format+".mp4.part";
var partPath = videoFolderPath+name+".f"+format+".mp4.part";
if (fs.existsSync(partPath))
{
const stats = fs.statSync(partPath);
@@ -142,7 +185,7 @@ function getAmountDownloadedMp4(name)
function getVideoFormatID(name)
{
var jsonPath = videoPath+name+".info.json";
var jsonPath = videoFolderPath+name+".info.json";
if (fs.existsSync(jsonPath))
{
var obj = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
@@ -151,29 +194,317 @@ function getVideoFormatID(name)
}
}
async function createPlaylistZipFile(fileNames, type, outputName) {
return new Promise(async resolve => {
let zipFolderPath = path.join(__dirname, (type === 'audio') ? audioFolderPath : videoFolderPath);
// let name = fileNames[0].split(' ')[0] + fileNames[1].split(' ')[0];
let ext = (type === 'audio') ? '.mp3' : '.mp4';
let output = fs.createWriteStream(path.join(zipFolderPath, outputName + '.zip'));
var archive = archiver('zip', {
gzip: true,
zlib: { level: 9 } // Sets the compression level.
});
archive.on('error', function(err) {
console.log(err);
throw err;
});
// pipe archive data to the output file
archive.pipe(output);
for (let i = 0; i < fileNames.length; i++) {
let fileName = fileNames[i];
archive.file(zipFolderPath + fileName + ext, {name: fileName + ext})
}
await archive.finalize();
// wait a tiny bit for the zip to reload in fs
setTimeout(function() {
resolve(path.join(zipFolderPath,outputName + '.zip'));
}, 100);
});
}
function deleteAudioFile(name) {
return new Promise(resolve => {
// TODO: split descriptors into audio and video descriptors, as deleting an audio file will close all video file streams
var jsonPath = path.join(audioFolderPath,name+'.mp3.info.json');
var audioFilePath = path.join(audioFolderPath,name+'.mp3');
jsonPath = path.join(__dirname, jsonPath);
audioFilePath = path.join(__dirname, audioFilePath);
let jsonExists = fs.existsSync(jsonPath);
let audioFileExists = fs.existsSync(audioFilePath);
if (descriptors[name]) {
try {
for (let i = 0; i < descriptors[name].length; i++) {
descriptors[name][i].destroy();
}
} catch(e) {
}
}
if (jsonExists) fs.unlinkSync(jsonPath);
if (audioFileExists) {
fs.unlink(audioFilePath, function(err) {
if (fs.existsSync(jsonPath) || fs.existsSync(audioFilePath)) {
resolve(false);
} else {
resolve(true);
}
});
} else {
// TODO: tell user that the file didn't exist
resolve(true);
}
});
}
async function deleteVideoFile(name) {
return new Promise(resolve => {
var jsonPath = path.join(videoFolderPath,name+'.info.json');
var videoFilePath = path.join(videoFolderPath,name+'.mp4');
jsonPath = path.join(__dirname, jsonPath);
videoFilePath = path.join(__dirname, videoFilePath);
jsonExists = fs.existsSync(jsonPath);
videoFileExists = fs.existsSync(videoFilePath);
if (descriptors[name]) {
try {
for (let i = 0; i < descriptors[name].length; i++) {
descriptors[name][i].destroy();
}
} catch(e) {
}
}
if (jsonExists) fs.unlinkSync(jsonPath);
if (videoFileExists) {
fs.unlink(videoFilePath, function(err) {
if (fs.existsSync(jsonPath) || fs.existsSync(videoFilePath)) {
resolve(false);
} else {
resolve(true);
}
});
} else {
// TODO: tell user that the file didn't exist
resolve(true);
}
});
}
function getAudioInfos(fileNames) {
let result = [];
for (let i = 0; i < fileNames.length; i++) {
let fileName = fileNames[i];
let fileLocation = audioFolderPath+fileName+'.mp3.info.json';
if (fs.existsSync(fileLocation)) {
let data = fs.readFileSync(fileLocation);
try {
result.push(JSON.parse(data));
} catch(e) {
console.log(`ERROR: Could not find info for file ${fileName}.mp3`);
}
}
}
return result;
}
function getVideoInfos(fileNames) {
let result = [];
for (let i = 0; i < fileNames.length; i++) {
let fileName = fileNames[i];
let fileLocation = videoFolderPath+fileName+'.info.json';
if (fs.existsSync(fileLocation)) {
let data = fs.readFileSync(fileLocation);
try {
result.push(JSON.parse(data));
} catch(e) {
console.log(`ERROR: Could not find info for file ${fileName}.mp4`);
}
}
}
return result;
}
// currently only works for single urls
async function getUrlInfos(urls) {
let result = [];
return new Promise(resolve => {
youtubedl.exec(urls.join(' '), ['--external-downloader', 'aria2c', '--dump-json'], {}, (err, output) => {
if (err) {
console.log('Error during parsing:' + err);
resolve(null);
}
let try_putput = null;
try {
try_putput = JSON.parse(output);
result = try_putput;
} catch(e) {
// probably multiple urls
console.log('failed to parse');
console.log(output);
}
resolve(result);
});
});
}
app.post('/tomp3', function(req, res) {
var url = req.body.url;
var date = Date.now();
var path = audioFolderPath;
var audiopath = '%(title)s';
var customQualityConfiguration = req.body.customQualityConfiguration;
var maxBitrate = req.body.maxBitrate;
let downloadConfig = ['--external-downloader', 'aria2c', '-o', path + audiopath + ".mp3", '-x', '--audio-format', 'mp3', '--write-info-json', '--print-json']
let qualityPath = '';
if (customQualityConfiguration) {
qualityPath = `-f ${customQualityConfiguration}`;
} else if (maxBitrate) {
if (!maxBitrate || maxBitrate === '') maxBitrate = '0';
qualityPath = `--audio-quality ${maxBitrate}`
}
if (qualityPath !== '') {
downloadConfig.splice(2, 0, qualityPath);
}
youtubedl.exec(url, downloadConfig, {}, function(err, output) {
if (debugMode) {
let new_date = Date.now();
let difference = (new_date - date)/1000;
console.log(`Audio download delay: ${difference} seconds.`);
}
if (err) {
audiopath = "-1";
res.sendStatus(500);
throw err;
} else if (output) {
var file_names = [];
for (let i = 0; i < output.length; i++) {
let output_json = null;
try {
output_json = JSON.parse(output[i]);
} catch(e) {
output_json = null;
}
if (!output_json) {
// only run on first go
return;
}
var file_name = output_json['_filename'].replace(/^.*[\\\/]/, '');
var alternate_file_name = file_name.substring(0, file_name.length-4);
if (alternate_file_name) file_names.push(alternate_file_name);
}
let is_playlist = file_names.length > 1;
if (!is_playlist) audiopath = file_names[0];
var audiopathEncoded = encodeURIComponent(file_names[0]);
res.send({
audiopathEncoded: audiopathEncoded,
file_names: is_playlist ? file_names : null
});
}
});
});
app.post('/tomp4', function(req, res) {
var url = req.body.url;
var date = Date.now();
var path = videoPath;
var videopath = Date.now();
youtubedl.exec(url, ['-o', path + videopath + ".mp4", '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4', '--write-info-json'], {}, function(err, output) {
var path = videoFolderPath;
var videopath = '%(title)s';
var selectedHeight = req.body.selectedHeight;
var customQualityConfiguration = req.body.customQualityConfiguration;
let qualityPath = 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4';
if (customQualityConfiguration) {
qualityPath = customQualityConfiguration;
} else if (selectedHeight && selectedHeight !== '') {
qualityPath = `bestvideo[height=${selectedHeight}]+bestaudio/best[height=${selectedHeight}]`;
}
youtubedl.exec(url, ['--external-downloader', 'aria2c', '-o', path + videopath + ".mp4", '-f', qualityPath, '--write-info-json', '--print-json'], {}, function(err, output) {
if (debugMode) {
let new_date = Date.now();
let difference = (new_date - date)/1000;
console.log(`Video download delay: ${difference} seconds.`);
}
if (err) {
videopath = "-1";
res.sendStatus(500);
throw err;
} else if (output) {
var file_names = [];
for (let i = 0; i < output.length; i++) {
let output_json = null;
try {
output_json = JSON.parse(output[i]);
} catch(e) {
output_json = null;
}
var modified_file_name = output_json ? output_json['title'] : null;
if (!output_json) {
continue;
}
var file_name = output_json['_filename'].replace(/^.*[\\\/]/, '');
// renames file if necessary due to bug
if (!fs.existsSync(output_json['_filename'] && fs.existsSync(output_json['_filename'] + '.webm'))) {
try {
fs.renameSync(output_json['_filename'] + '.webm', output_json['_filename']);
console.log('Renamed ' + file_name + '.webm to ' + file_name);
} catch(e) {
}
}
var alternate_file_name = file_name.substring(0, file_name.length-4);
if (alternate_file_name) file_names.push(alternate_file_name);
}
let is_playlist = file_names.length > 1;
if (!is_playlist) audiopath = file_names[0];
var videopathEncoded = encodeURIComponent(file_names[0]);
res.send({
videopathEncoded: videopathEncoded,
file_names: is_playlist ? file_names : null
});
res.end("yes");
}
});
var completeString = "done";
var videopathEncoded = encodeURIComponent(videopath);
res.send(videopathEncoded);
res.end("yes");
});
app.post('/mp3fileexists', function(req, res) {
var name = req.body.name + "";
// gets the status of the mp3 file that's being downloaded
app.post('/fileStatusMp3', function(req, res) {
var name = decodeURI(req.body.name + "");
var exists = "";
var fullpath = audioPath + name + ".mp3";
var fullpath = audioFolderPath + name + ".mp3";
if (fs.existsSync(fullpath)) {
exists = [basePath + audioPath + name, getFileSizeMp3(name)];
exists = [basePath + audioFolderPath + name, getFileSizeMp3(name)];
}
else
{
@@ -189,15 +520,14 @@ app.post('/mp3fileexists', function(req, res) {
res.end("yes");
});
app.post('/mp4fileexists', function(req, res) {
var name = req.body.name;
// gets the status of the mp4 file that's being downloaded
app.post('/fileStatusMp4', function(req, res) {
var name = decodeURI(req.body.name);
var exists = "";
var fullpath = videoPath + name + ".mp4";
var fullpath = videoFolderPath + name + ".mp4";
if (fs.existsSync(fullpath)) {
exists = [basePath + videoPath + name, getFileSizeMp4(name)];
}
else
{
exists = [basePath + videoFolderPath + name, getFileSizeMp4(name)];
} else {
var percent = 0;
var size = getFileSizeMp4(name);
var downloaded = getAmountDownloadedMp4(name);
@@ -210,9 +540,199 @@ app.post('/mp4fileexists', function(req, res) {
res.end("yes");
});
// gets all download mp3s
app.post('/getMp3s', function(req, res) {
var mp3s = [];
var playlists = db.get('playlists.audio').value();
var fullpath = audioFolderPath;
var files = fs.readdirSync(audioFolderPath);
for (var i in files)
{
var nameLength = path.basename(files[i]).length;
var ext = path.basename(files[i]).substring(nameLength-4, nameLength);
if (ext == ".mp3")
{
var jsonobj = getJSONMp3(path.basename(files[i]).substring(0, path.basename(files[i]).length-4));
if (!jsonobj) continue;
var id = path.basename(files[i]).substring(0, path.basename(files[i]).length-4);
var title = jsonobj.title;
if (title.length > 14) // edits title if it's too long
{
title = title.substring(0,12) + "...";
}
var thumbnail = jsonobj.thumbnail;
var duration = jsonobj.duration;
var isaudio = true;
var file = new File(id, title, thumbnail, isaudio, duration);
mp3s.push(file);
}
}
res.send({
mp3s: mp3s,
playlists: playlists
});
res.end("yes");
});
// gets all download mp4s
app.post('/getMp4s', function(req, res) {
var mp4s = [];
var playlists = db.get('playlists.video').value();
var fullpath = videoFolderPath;
var files = fs.readdirSync(videoFolderPath);
for (var i in files)
{
var nameLength = path.basename(files[i]).length;
var ext = path.basename(files[i]).substring(nameLength-4, nameLength);
if (ext == ".mp4")
{
var jsonobj = getJSONMp4(path.basename(files[i]).substring(0, path.basename(files[i]).length-4));
if (!jsonobj) continue;
var id = path.basename(files[i]).substring(0, path.basename(files[i]).length-4);
var title = jsonobj.title;
if (title.length > 14) // edits title if it's too long
{
title = title.substring(0,12) + "...";
}
var thumbnail = jsonobj.thumbnail;
var duration = jsonobj.duration;
var isaudio = false;
var file = new File(id, title, thumbnail, isaudio, duration);
mp4s.push(file);
}
}
res.send({
mp4s: mp4s,
playlists: playlists
});
res.end("yes");
});
app.post('/createPlaylist', async (req, res) => {
let playlistName = req.body.playlistName;
let fileNames = req.body.fileNames;
let type = req.body.type;
let thumbnailURL = req.body.thumbnailURL;
let new_playlist = {
'name': playlistName,
fileNames: fileNames,
id: shortid.generate(),
thumbnailURL: thumbnailURL
};
db.get(`playlists.${type}`)
.push(new_playlist)
.write();
res.send({
new_playlist: new_playlist,
success: !!new_playlist // always going to be true
})
});
app.post('/deletePlaylist', async (req, res) => {
let playlistID = req.body.playlistID;
let type = req.body.type;
let success = null;
try {
// removes playlist from playlists
db.get(`playlists.${type}`)
.remove({id: playlistID})
.write();
success = true;
} catch(e) {
success = false;
}
res.send({
success: success
})
});
// deletes mp3 file
app.post('/deleteMp3', async (req, res) => {
var name = req.body.name;
var fullpath = audioFolderPath + name + ".mp3";
var wasDeleted = false;
if (fs.existsSync(fullpath))
{
deleteAudioFile(name);
wasDeleted = true;
res.send(wasDeleted);
res.end("yes");
}
else
{
wasDeleted = false;
res.send(wasDeleted);
res.end("yes");
}
});
// deletes mp4 file
app.post('/deleteMp4', async (req, res) => {
var name = req.body.name;
var fullpath = videoFolderPath + name + ".mp4";
var wasDeleted = false;
if (fs.existsSync(fullpath))
{
wasDeleted = await deleteVideoFile(name);
// wasDeleted = true;
res.send(wasDeleted);
res.end("yes");
}
else
{
wasDeleted = false;
res.send(wasDeleted);
res.end("yes");
}
});
app.post('/downloadFile', async (req, res) => {
let fileNames = req.body.fileNames;
let is_playlist = req.body.is_playlist;
let type = req.body.type;
let outputName = req.body.outputName;
let file = null;
if (!is_playlist) {
if (type === 'audio') {
file = __dirname + '/' + 'audio/' + fileNames + '.mp3';
} else if (type === 'video') {
file = __dirname + '/' + 'video/' + fileNames + '.mp4';
}
} else {
file = await createPlaylistZipFile(fileNames, type, outputName);
}
res.sendFile(file);
});
app.post('/deleteFile', async (req, res) => {
let fileName = req.body.fileName;
let type = req.body.type;
if (type === 'audio') {
deleteAudioFile(fileName);
} else if (type === 'video') {
deleteVideoFile(fileName);
}
res.send()
});
app.get('/video/:id', function(req , res){
var head;
const path = "video/" + req.params.id + ".mp4";
const path = "video/" + req.params.id + '.mp4';
const stat = fs.statSync(path)
const fileSize = stat.size
const range = req.headers.range
@@ -224,6 +744,13 @@ app.get('/video/:id', function(req , res){
: fileSize-1
const chunksize = (end-start)+1
const file = fs.createReadStream(path, {start, end})
if (descriptors[req.params.id]) descriptors[req.params.id].push(file);
else descriptors[req.params.id] = [file];
file.on('close', function() {
let index = descriptors[req.params.id].indexOf(file);
descriptors[req.params.id].splice(index, 1);
if (debugMode) console.log('Successfully closed stream and removed file reference.');
});
head = {
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'Accept-Ranges': 'bytes',
@@ -244,7 +771,8 @@ app.get('/video/:id', function(req , res){
app.get('/audio/:id', function(req , res){
var head;
const path = "audio/" + req.params.id + ".mp3";
let path = "audio/" + req.params.id + '.mp3';
path = path.replace(/\"/g, '\'');
const stat = fs.statSync(path)
const fileSize = stat.size
const range = req.headers.range
@@ -255,7 +783,14 @@ app.get('/audio/:id', function(req , res){
? parseInt(parts[1], 10)
: fileSize-1
const chunksize = (end-start)+1
const file = fs.createReadStream(path, {start, end})
const file = fs.createReadStream(path, {start, end});
if (descriptors[req.params.id]) descriptors[req.params.id].push(file);
else descriptors[req.params.id] = [file];
file.on('close', function() {
let index = descriptors[req.params.id].indexOf(file);
descriptors[req.params.id].splice(index, 1);
if (debugMode) console.log('Successfully closed stream and removed file reference.');
});
head = {
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'Accept-Ranges': 'bytes',
@@ -275,6 +810,27 @@ app.get('/audio/:id', function(req , res){
});
app.post('/getVideoInfos', async (req, res) => {
let fileNames = req.body.fileNames;
let urlMode = !!req.body.urlMode;
let type = req.body.type;
let result = null;
if (!urlMode) {
if (type === 'audio') {
result = getAudioInfos(fileNames)
} else if (type === 'video') {
result = getVideoInfos(fileNames);
}
} else {
result = await getUrlInfos(fileNames);
}
res.send({
result: result,
success: !!result
})
});
if (usingEncryption)

BIN
backend/aria2c.exe Normal file

Binary file not shown.

View File

@@ -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/"
}
}
}

View File

@@ -1,21 +1,32 @@
{
"YoutubeDLMaterial": {
"Host": {
"frontendurl": "http://localhost:4200",
"backendurl": "https://youtubedl.grynsztein.com/"
"frontendurl": "http://example.com",
"backendurl": "http://example.com: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-base": "http://example.com:17442/",
"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
},
"API": {
"use_youtube_API": false,
"youtube_API_key": ""
},
"Themes": {
"default_theme": "default",
"allow_theme_change": true
}
}
}
}

View File

@@ -0,0 +1,32 @@
{
"YoutubeDLMaterial": {
"Host": {
"frontendurl": "https://example.com",
"backendurl": "https://example.com: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-base": "https://example.com:17442/",
"path-audio": "audio/",
"path-video": "video/"
},
"Extra": {
"title_top": "Youtube Downloader",
"file_manager_enabled": true,
"allow_quality_select": true,
"download_only_mode": false
},
"API": {
"use_youtube_API": false,
"youtube_API_key": ""
},
"Themes": {
"default_theme": "default",
"allow_theme_change": true
}
}
}

View File

@@ -1,23 +1,29 @@
{
"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"
},
"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",
"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": "^2.3.0"
}
}

Binary file not shown.

12
browserslist Normal file
View 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'.

View 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
View 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();
}
});

View File

@@ -8,44 +8,57 @@
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
"e2e": "ng e2e",
"electron": "ng build --base-href ./ && electron ."
},
"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"
},
"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
View 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);

View 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 { }

View File

@@ -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;
}

View File

@@ -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%; 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>

View File

@@ -1,176 +1,119 @@
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;
// config items
topBarTitle = 'Youtube Downloader';
defaultTheme = null;
allowThemeChange = null;
@ViewChild('urlinput', { read: ElementRef, static: false }) urlInput: ElementRef;
constructor(public postsService: PostsService, public snackBar: MatSnackBar,
public router: Router, public overlayContainer: OverlayContainer) {
// loading config
this.postsService.loadNavItems().subscribe(result => { // loads settings
var backendUrl = result.YoutubeDLMaterial.Host.backendurl;
this.topBarTitle = result.YoutubeDLMaterial.Extra.title_top;
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;
this.postsService.path = backendUrl;
this.postsService.startPath = backendUrl;
this.postsService.startPathSSL = backendUrl;
},
error => {
// 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);
} 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']);
}
}

View File

@@ -1,19 +1,39 @@
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,
MatGridList,
MatProgressSpinnerModule,
MatButtonToggleModule,
MatDialogModule} from '@angular/material';
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 } from 'ng-lazyload-image';
import { NgxContentLoadingModule } from 'ngx-content-loading';
@NgModule({
declarations: [
AppComponent
AppComponent,
FileCardComponent,
MainComponent,
PlayerComponent,
InputDialogComponent
],
imports: [
BrowserModule,
@@ -34,7 +54,23 @@ import {APP_BASE_HREF} from '@angular/common';
MatSidenavModule,
MatIconModule,
MatListModule,
MatProgressBarModule
MatGridListModule,
MatExpansionModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatButtonToggleModule,
MatDialogModule,
VgCoreModule,
VgControlsModule,
VgOverlayPlayModule,
VgBufferingModule,
LazyLoadImageModule,
NgxContentLoadingModule,
RouterModule,
AppRoutingModule,
],
entryComponents: [
InputDialogComponent
],
providers: [PostsService],
bootstrap: [AppComponent]

View 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;
}
}

View File

@@ -0,0 +1,21 @@
<mat-card class="example-card mat-elevation-z6">
<button (click)="deleteFile()" class="deleteButton" mat-icon-button><mat-icon>delete_forever</mat-icon></button>
<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 class="img-div">
<img class="image" [lazyLoad]="thumbnailURL" (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>
</mat-card>

View 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();
});
});

View File

@@ -0,0 +1,57 @@
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';
@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;
constructor(private postsService: PostsService, public snackBar: MatSnackBar, public mainComponent: MainComponent) { }
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);
}
}
imageLoaded(loaded) {
this.image_loaded = true;
}
public openSnackBar(message: string, action: string) {
this.snackBar.open(message, action, {
duration: 2000,
});
}
}

View File

@@ -0,0 +1,3 @@
.mat-spinner {
margin-left: 5%;
}

View 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>

View 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();
});
});

View 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);
}
}
}
}

View File

@@ -0,0 +1,114 @@
.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;
}

View File

@@ -0,0 +1,154 @@
<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] && cachedAvailableFormats[url][(audioOnly) ? 'audio' : 'video'][option.value]" [value]="option.value">
{{option.label}}
</mat-option>
</ng-container>
</mat-select>
<div class="spinner-div" *ngIf="formats_loading && !cachedAvailableFormats[url]">
<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 (change)="videoModeChanged($event)" [(ngModel)]="audioOnly" style="float: left; margin-top: -12px">Only Audio</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>
</mat-card-actions>
</mat-card>
</div>
<br/>
<div class="centered big" id="bar_div" *ngIf="downloadingfile;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 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 (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 *ngIf="playlists.audio.length > 0"></mat-divider>
<div style="width: 100%; text-align: center; margin-top: 10px;" *ngIf="playlists.audio.length > 0">
<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 (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>
</mat-expansion-panel>
<mat-expansion-panel 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 (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 *ngIf="playlists.video.length > 0"></mat-divider>
<div style="width: 100%; text-align: center; margin-top: 10px;" *ngIf="playlists.video.length > 0">
<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 (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>
</div>
</mat-expansion-panel>
</mat-accordion>
</div>
<ng-template #nomp3s>
</ng-template>
<ng-template #nomp4s>
</ng-template>

View 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();
});
});

View File

@@ -0,0 +1,657 @@
import { Component, OnInit, ElementRef, ViewChild } 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';
@Component({
selector: 'app-root',
templateUrl: './main.component.html',
styleUrls: ['./main.component.css']
})
export class MainComponent implements OnInit {
iOS = false;
determinateProgress = false;
downloadingfile = false;
audioOnly: boolean;
urlError = false;
path = '';
url = '';
exists = '';
percentDownloaded: number;
// settings
fileManagerEnabled = false;
allowQualitySelect = false;
downloadOnlyMode = false;
baseStreamPath;
audioFolderPath;
videoFolderPath;
cachedAvailableFormats = {};
// youtube api
youtubeSearchEnabled = false;
youtubeAPIKey = null;
results_loading = false;
results_showing = true;
results = [];
mp3s: any[] = [];
mp4s: any[] = [];
files_cols = (window.innerWidth <= 450) ? 2 : 4;
playlists = {'audio': [], 'video': []};
playlist_thumbnails = {};
downloading_content = {'audio': {}, 'video': {}};
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;
last_valid_url = '';
last_url_check = 0;
constructor(private postsService: PostsService, private youtubeSearch: YoutubeSearchService, public snackBar: MatSnackBar,
private router: Router) {
this.audioOnly = false;
// loading config
this.postsService.loadNavItems().subscribe(result => { // loads settings
const backendUrl = result['YoutubeDLMaterial']['Host']['backendurl'];
this.fileManagerEnabled = result['YoutubeDLMaterial']['Extra']['file_manager_enabled'];
this.downloadOnlyMode = result['YoutubeDLMaterial']['Extra']['download_only_mode'];
this.baseStreamPath = result['YoutubeDLMaterial']['Downloader']['path-base'];
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.postsService.path = backendUrl;
this.postsService.startPath = backendUrl;
this.postsService.startPathSSL = backendUrl;
if (this.fileManagerEnabled) {
this.getMp3s();
this.getMp4s();
}
if (this.youtubeSearchEnabled && this.youtubeAPIKey) {
this.youtubeSearch.initializeAPI(this.youtubeAPIKey);
this.attachToInput();
}
}, error => {
console.log(error);
});
}
// file manager stuff
getMp3s() {
this.postsService.getMp3s().subscribe(result => {
const mp3s = result['mp3s'];
const playlists = result['playlists'];
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];
}
}
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'];
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];
}
}
this.playlist_thumbnails[playlist.id] = videoToExtractThumbnail.thumbnailURL;
}
},
error => {
console.log(error);
});
}
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();
});
}
// app initialization.
ngOnInit() {
this.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window['MSStream'];
if (localStorage.getItem('audioOnly') !== null) {
this.audioOnly = localStorage.getItem('audioOnly') === 'true';
}
}
// download helpers
downloadHelperMp3(name, is_playlist = false, forceView = false) {
this.downloadingfile = false;
// 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'}]);
// window.location.href = this.baseStreamPath + this.audioFolderPath + name[0] + '.mp3';
} else {
this.router.navigate(['/player', {fileNames: name, type: 'audio'}]);
// window.location.href = this.baseStreamPath + this.audioFolderPath + name + '.mp3';
}
}
// reloads mp3s
if (this.fileManagerEnabled) {
this.getMp3s();
}
}
downloadHelperMp4(name, is_playlist = false, forceView = false) {
this.downloadingfile = false;
// 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'}]);
// window.location.href = this.baseStreamPath + this.videoFolderPath + name[0] + '.mp4';
} else {
this.router.navigate(['/player', {fileNames: name, type: 'video'}]);
// window.location.href = this.baseStreamPath + this.videoFolderPath + name + '.mp4';
}
}
// reloads mp4s
if (this.fileManagerEnabled) {
this.getMp4s();
}
}
// download click handler
downloadClicked() {
if (this.ValidURL(this.url)) {
this.urlError = false;
this.path = '';
if (this.audioOnly) {
this.downloadingfile = true;
let customQualityConfiguration = null;
if (this.selectedQuality !== '') {
const cachedFormatsExists = this.cachedAvailableFormats[this.url];
if (cachedFormatsExists) {
const audio_formats = this.cachedAvailableFormats[this.url]['audio'];
customQualityConfiguration = audio_formats[this.selectedQuality]['format_id'];
}
}
this.postsService.makeMP3(this.url, (this.selectedQuality === '' ? null : this.selectedQuality),
customQualityConfiguration).subscribe(posts => {
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);
}
}, error => { // can't access server
this.downloadingfile = false;
this.openSnackBar('Download failed!', 'OK.');
});
} else {
let customQualityConfiguration = null;
const cachedFormatsExists = this.cachedAvailableFormats[this.url];
if (cachedFormatsExists) {
const video_formats = this.cachedAvailableFormats[this.url]['video'];
if (video_formats['best_audio_format'] && this.selectedQuality !== '') {
customQualityConfiguration = video_formats[this.selectedQuality]['format_id'] + '+' + video_formats['best_audio_format'];
}
}
this.downloadingfile = true;
this.postsService.makeMP4(this.url, (this.selectedQuality === '' ? null : this.selectedQuality),
customQualityConfiguration).subscribe(posts => {
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);
}
}, error => { // can't access server
this.downloadingfile = false;
this.openSnackBar('Download failed!', 'OK.');
});
}
} else {
this.urlError = true;
}
}
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 = 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.formats_loading = true;
this.postsService.getFileInfo([url], 'irrelevant', true).subscribe(res => {
if (url === this.url) { this.formats_loading = false; }
const infos = res['result'];
const parsed_infos = this.getAudioAndVideoFormats(infos.formats);
const available_formats = {audio: parsed_infos[0], video: parsed_infos[1]};
this.cachedAvailableFormats[url] = available_formats;
});
}
}
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.files_cols = (event.target.innerWidth <= 450) ? 2 : 4;
}
videoModeChanged(new_val) {
this.selectedQuality = '';
localStorage.setItem('audioOnly', new_val.checked.toString());
}
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;
}
}

View File

@@ -0,0 +1,61 @@
.video-player {
margin: 0 auto;
}
.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;
}

View File

@@ -0,0 +1,25 @@
<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 style="width: 80%; left: 9%" vertical name="videoSelect" aria-label="Video Select" #group="matButtonToggleGroup">
<mat-button-toggle *ngFor="let name of fileNames; let i = index" [checked]="currentItem.title === name" (click)="onClickPlaylistItem(playlist[i], i)" class="toggle-button" [value]="name">{{decodeURI(name)}}</mat-button-toggle>
</mat-button-toggle-group>
</div>
</div>
</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>

View 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();
});
});

View File

@@ -0,0 +1,218 @@
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';
export interface IMedia {
title: string;
src: string;
type: string;
}
@Component({
selector: 'app-player',
templateUrl: './player.component.html',
styleUrls: ['./player.component.css']
})
export class PlayerComponent implements OnInit {
playlist: Array<IMedia> = [];
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(result => { // loads settings
this.baseStreamPath = result['YoutubeDLMaterial']['Downloader']['path-base'];
this.audioFolderPath = result['YoutubeDLMaterial']['Downloader']['path-audio'];
this.videoFolderPath = result['YoutubeDLMaterial']['Downloader']['path-video'];
const backendUrl = result['YoutubeDLMaterial']['Host']['backendurl'];
this.postsService.path = backendUrl;
this.postsService.startPath = backendUrl;
this.postsService.startPathSSL = backendUrl;
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 + fileName; // + (this.type === 'audio' ? '.mp3' : '.mp4');
const mediaObject: IMedia = {
title: fileName,
src: fullLocation,
type: fileType
}
this.playlist.push(mediaObject);
}
this.currentItem = this.playlist[this.currentIndex];
});
// 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() {
this.postsService.getFileInfo(this.fileNames, this.type, false).subscribe(res => {
});
}
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) {
this.postsService.createPlaylist(name, this.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);
}
// snackbar helper
public openSnackBar(message: string, action: string) {
this.snackBar.open(message, action, {
duration: 2000,
});
}
}

View File

@@ -1,74 +1,114 @@
import {Injectable} from '@angular/core';
import {Http} from '@angular/http';
import {Injectable, isDevMode} 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';
@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 = 'http://localhost:17442/';
startPathSSL = 'https://localhost:17442/'
handShakeComplete = false;
THEMES_CONFIG = THEMES_CONFIG;
theme;
constructor(private http: Http){
constructor(private http: HttpClient) {
console.log('PostsService Initialized...');
}
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());
makeMP3(url: string, selectedQuality: string, customQualityConfiguration: string) {
return this.http.post(this.path + 'tomp3', {url: url,
maxBitrate: selectedQuality,
customQualityConfiguration: customQualityConfiguration});
}
getFileStatusMp3(name: string): Observable<any> {
return this.http.post(this.path + "mp3fileexists",{name: name})
.map(res => res.json());
makeMP4(url: string, selectedQuality: string, customQualityConfiguration: string) {
return this.http.post(this.path + 'tomp4', {url: url,
selectedHeight: selectedQuality,
customQualityConfiguration: customQualityConfiguration});
}
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');
}
const locations = window.location.href.split('#');
const current_location = locations[0];
return this.http.get(current_location + 'backend/config/default.json');
}
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});
}
removePlaylist(playlistID, type) {
return this.http.post(this.path + 'deletePlaylist', {playlistID: playlistID, type: type});
}
}

View 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();
});
});

View 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;
}

32
src/assets/default.json Normal file
View File

@@ -0,0 +1,32 @@
{
"YoutubeDLMaterial": {
"Host": {
"frontendurl": "http://localhost:4200",
"backendurl": "http://localhost: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-base": "http://localhost:17442/",
"path-audio": "audio/",
"path-video": "video/"
},
"Extra": {
"title_top": "Youtube Downloader",
"file_manager_enabled": true,
"allow_quality_select": true,
"download_only_mode": false
},
"API": {
"use_youtube_API": false,
"youtube_API_key": ""
},
"Themes": {
"default_theme": "default",
"allow_theme_change": true
}
}
}

View File

@@ -7,10 +7,9 @@
<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 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" rel="stylesheet">
</head>
<body>

View File

@@ -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`.

View File

@@ -1 +0,0 @@
/* You can add global styles to this file, and also import other style files */

70
src/styles.scss Normal file
View 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
View 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};

View File

@@ -2,7 +2,6 @@
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"module": "es2015",
"baseUrl": "",
"types": []
},

View File

@@ -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"
]

View File

@@ -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"
}
}
}

View File

@@ -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,