Compare commits

...

64 Commits
v3.5.1 ... v3.6

Author SHA1 Message Date
Isaac Grynsztein
eddc25566d Updated behavior of file card deletion to prevent compilation error 2020-04-10 20:49:34 -04:00
Isaac Grynsztein
b5a82b9385 Updated middleware to support API tokens. Frontend now uses an admin token for its requests
Fixed version numbers
2020-04-10 20:44:42 -04:00
Isaac Grynsztein
2082a78846 Updated version number 2020-04-10 20:41:14 -04:00
Isaac Grynsztein
fe170a4de8 Updated public API link 2020-04-10 15:25:23 -04:00
Tzahi12345
18dab72b51 Updated public API 2020-04-10 15:24:26 -04:00
Isaac Grynsztein
6849bd00d5 Adding public API docs 2020-04-10 15:13:07 -04:00
Isaac Grynsztein
1e96e31053 Added new API key and using API key config items to enable a public API
API key config items are implemented UI-side

Added ability to generate API keys through the settings

Switched getmp3s and getmp4s api calls to be GET requests rather than POST

Removed unused code from settings dialog
2020-04-10 14:46:36 -04:00
Isaac Grynsztein
02441ac846 Fixed bug where docker would start building on certain systems when using docker-compose up, and refuse to pull with docker-compose pull 2020-04-10 01:33:36 -04:00
Tzahi12345
4666aa15b3 Merge pull request #49 from Tzahi12345/streaming-only-mode
Streaming-only mode
2020-04-09 23:41:44 -04:00
Isaac Grynsztein
f6f54c0e53 Fixed bug where video infos caused an error for streaming-only subscriptions 2020-04-09 23:41:21 -04:00
Isaac Grynsztein
e15141c5e0 Added backend and database support for video streaming
Added UI support for video streaming. branch is now feature-complete
2020-04-09 23:33:58 -04:00
Isaac Grynsztein
a61606b69f Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into streaming-only-mode 2020-04-09 22:29:56 -04:00
Isaac Grynsztein
d96fab49f5 added ability to use url in player component. streaming mode will need this 2020-04-09 22:29:23 -04:00
Isaac Grynsztein
346d41d3e1 Fixed bug that broke server backups 2020-04-09 01:57:43 -04:00
Isaac Grynsztein
597e1f5b60 Fixed bug that prevented updates from succeeding 2020-04-09 01:47:10 -04:00
Isaac Grynsztein
dd0f58d421 Fixed bug where updater kept asking for updates on the update after it completed 2020-04-09 01:36:46 -04:00
Isaac Grynsztein
2ca6aa7bd7 Video URL now wraps in video info dialog 2020-04-09 01:36:02 -04:00
Isaac Grynsztein
ba2b837cc5 Fixed accidentally commented out functionality to download new updates 2020-04-09 01:35:22 -04:00
Isaac Grynsztein
22f0ee834b backups now occur in appdata/backups folder 2020-04-08 15:58:29 -04:00
Isaac Grynsztein
cb02227302 fixed bug where recently saved playlists could not be shared 2020-04-07 22:08:18 -04:00
Isaac Grynsztein
1b4f2830f5 changed logging in backend to support extra logging in debug mode 2020-04-07 21:41:13 -04:00
Isaac Grynsztein
720fceefb6 Fixed bug where subscription videos could not be downloaded from the player 2020-04-07 19:16:15 -04:00
Isaac Grynsztein
fa488015c3 Minor import change 2020-04-07 18:43:27 -04:00
Tzahi12345
88f1b3daff Merge pull request #48 from Tzahi12345/video-sharing
Video sharing
2020-04-07 18:29:29 -04:00
Isaac Grynsztein
0ea8a11c85 Subscriptions now don't have a share button (it's broken anyways) 2020-04-07 18:26:47 -04:00
Isaac Grynsztein
14bf2248cf Added UI support for sharing videos 2020-04-07 18:19:12 -04:00
Isaac Grynsztein
822aec4de8 added API endpoint to get file from database
video/audio files can now be retrieved by just uid, allowing for easy sharing

added API endpoints for sharing/unsharing a video (no UI support yet)
2020-04-07 14:49:05 -04:00
Isaac Grynsztein
2a1aa4036c New checkbox to select streaming only mode when subscribing 2020-04-07 01:52:59 -04:00
Isaac Grynsztein
2414e16021 videos now deleted by UID ui-side 2020-04-07 01:52:22 -04:00
Isaac Grynsztein
69cd22d992 file deletions now remove the file from the db as well 2020-04-07 01:38:35 -04:00
Isaac Grynsztein
1905129201 getMp3s and getMp4s now have dedicated functions
downloaded files now get recorded in db.json. So when the server wants to get audio/video files, it doesn't need to recursively go through the respective folders each time
- getMp4s/getMp3s API request latency is reduced ~2x (130ms -> 60ms) in testing

Modified tomp3/tomp4 code to automatically add newly downloaded files to the db

Added a migration so users on 3.5 or below will get their files automatically added to the db on the first run

All these changes are necessary to enable easy sharing with features like timestamps
2020-04-07 00:00:25 -04:00
Isaac Grynsztein
7ef6c78612 merged new checkbox for bookmarklet: enables ability to set bookmarklet to audio only
fixed two bugs for audio only files: sometimes downloads failed as extensions were improperly removed and readded, removing a single character from the filename.

Fixed another extension-related bug where metadata from deleted audio files persisted
2020-04-05 15:19:12 -04:00
Tzahi12345
1d9595d056 Merge pull request #47 from Tzahi12345/better-settings-menu
Better settings menu
2020-04-04 23:35:13 -04:00
Isaac Grynsztein
d258bc2218 Updated appearance of settings menu to improve organization and performance 2020-04-04 23:29:42 -04:00
Isaac Grynsztein
4d3a687d34 Fixed bug where toggling dark mode using the toggle rather than the adjacent menu item caused visual errors 2020-04-04 23:28:34 -04:00
Tzahi12345
2b91293abd Merge pull request #45 from Tzahi12345/better-logging
Better logging system using winston
2020-04-02 23:16:31 -04:00
Isaac Grynsztein
3990e25c18 added logging to config api and subscriptions api, meaning the entire backend has the new logging system 2020-04-02 23:14:07 -04:00
Isaac Grynsztein
2f0bbca15c added better logging in app.js using winston 2020-04-02 23:05:17 -04:00
Tzahi12345
717f6deb11 Merge pull request #44 from Tzahi12345/auto-update-youtubedl-material
Ability to update YoutubeDL-Material
2020-04-02 21:56:44 -04:00
Isaac Grynsztein
c36867d368 Added progress bar to file downloads
Added two new API calls, to update the server to a particular version and to get the updater status

You can now update through the UI, and a status dialog displays after
2020-04-02 21:53:08 -04:00
Isaac Grynsztein
458e4b45f8 Removed @locl dependency for translations
Added CommonModule to fix intellisense

Added ability to load json assets by name, and an http call to update youtubedl-material
2020-04-01 19:44:22 -04:00
Isaac Grynsztein
c40513ba4a docker-compose now uses latest version tag 2020-04-01 19:42:44 -04:00
Isaac Grynsztein
6fa52cecbc Updated docker compose version 2020-04-01 19:42:21 -04:00
Isaac Grynsztein
a5474141bb Removed unused dependencies 2020-04-01 19:36:57 -04:00
Isaac Grynsztein
89ececdbeb Dependencies now install during update 2020-04-01 19:31:41 -04:00
Isaac Grynsztein
58718b6e3b Removed @ngular/http dependency 2020-04-01 01:11:26 -04:00
Isaac Grynsztein
a5224f80a8 nodemon now runs silently 2020-04-01 01:07:08 -04:00
Isaac Grynsztein
c2ee6b6230 update package.json version 2020-04-01 01:06:55 -04:00
Isaac Grynsztein
37614a1611 Changed backend logging for server start to give more information (namely version) 2020-03-31 04:20:46 -04:00
Isaac Grynsztein
b71bdfcec2 Updated nodemon package.json config 2020-03-31 04:19:42 -04:00
Isaac Grynsztein
1b09bf4881 nodemon now supported 2020-03-31 01:56:15 -04:00
Tzahi12345
82df232f03 Update process now properly gets required backend files 2020-03-30 23:36:35 -04:00
Tzahi12345
af4de44016 Further merge 2020-03-30 23:25:58 -04:00
Tzahi12345
61f27d6fe9 merged changes 2020-03-30 23:25:48 -04:00
Tzahi12345
b3dbdd1790 Cleaning working folder 2020-03-30 23:24:33 -04:00
Isaac Grynsztein
785306c59a Added debug statements 2020-03-30 23:20:52 -04:00
Tzahi12345
38774d8593 Updater now grab new backend files
youtube-dl auto updater now guesses binary path, which makes the update process work much more reliably
2020-03-30 23:15:31 -04:00
Isaac Grynsztein
df11aca1e0 Added preliminary support for updating YoutubeDL-Material
Config items that are not found use and set the default value

Fixed potential error while updated youtube-dl binaries
2020-03-30 18:35:44 -04:00
Isaac Grynsztein
bcff631936 Updated translations and 'backend/public' folder 2020-03-29 10:46:35 -04:00
Isaac Grynsztein
347df89aa7 Updated default title_top value in config 2020-03-29 10:45:31 -04:00
Isaac Grynsztein
eb084d03b2 Downloaded file names are now converted to their fully decoded forms 2020-03-28 08:04:45 -04:00
Isaac Grynsztein
8c942b0343 Updated top bar color 2020-03-27 20:02:18 -04:00
Isaac Grynsztein
baad97182a Updated version tag 2020-03-27 20:02:06 -04:00
Isaac Grynsztein
26ca5d51a5 Fixed about dialog translations 2020-03-27 16:59:50 -04:00
63 changed files with 3447 additions and 967 deletions

2
.gitignore vendored
View File

@@ -61,3 +61,5 @@ backend/appdata/archives/archive_audio.txt
backend/appdata/archives/archive_video.txt
backend/appdata/archives/blacklist_audio.txt
backend/appdata/archives/blacklist_video.txt
backend/appdata/logs/combined.log
backend/appdata/logs/error.log

1208
Public API v1.yaml Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +1,48 @@
{
"YoutubeDLMaterial": {
"Host": {
"url": "http://example.com",
"port": "17442"
},
"Encryption": {
"use-encryption": false,
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
},
"Downloader": {
"path-audio": "audio/",
"path-video": "video/",
"use_youtubedl_archive": false,
"custom_args": ""
},
"Extra": {
"title_top": "Youtube Downloader",
"file_manager_enabled": true,
"allow_quality_select": true,
"download_only_mode": false,
"allow_multi_download_mode": true,
"settings_pin_required": false
},
"API": {
"use_youtube_API": false,
"youtube_API_key": ""
},
"Themes": {
"default_theme": "default",
"allow_theme_change": true
},
"Subscriptions": {
"allow_subscriptions": true,
"subscriptions_base_path": "subscriptions/",
"subscriptions_check_interval": "300",
"subscriptions_use_youtubedl_archive": true
},
"Advanced": {
"use_default_downloading_agent": true,
"custom_downloading_agent": "",
"allow_advanced_download": false
}
"YoutubeDLMaterial": {
"Host": {
"url": "http://example.com",
"port": "17442"
},
"Encryption": {
"use-encryption": false,
"cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem",
"key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem"
},
"Downloader": {
"path-audio": "audio/",
"path-video": "video/",
"use_youtubedl_archive": false,
"custom_args": ""
},
"Extra": {
"title_top": "YoutubeDL-Material",
"file_manager_enabled": true,
"allow_quality_select": true,
"download_only_mode": false,
"allow_multi_download_mode": true,
"settings_pin_required": false
},
"API": {
"use_API_key": false,
"API_key": "",
"use_youtube_API": false,
"youtube_API_key": ""
},
"Themes": {
"default_theme": "default",
"allow_theme_change": true
},
"Subscriptions": {
"allow_subscriptions": true,
"subscriptions_base_path": "subscriptions/",
"subscriptions_check_interval": "300",
"subscriptions_use_youtubedl_archive": true
},
"Advanced": {
"use_default_downloading_agent": true,
"custom_downloading_agent": "",
"allow_advanced_download": false
}
}
}

View File

@@ -16,7 +16,7 @@
"custom_args": ""
},
"Extra": {
"title_top": "Youtube Downloader",
"title_top": "YoutubeDL-Material",
"file_manager_enabled": true,
"allow_quality_select": true,
"download_only_mode": false,
@@ -24,6 +24,8 @@
"settings_pin_required": false
},
"API": {
"use_API_key": false,
"API_key": "",
"use_youtube_API": false,
"youtube_API_key": ""
},

View File

@@ -5,6 +5,9 @@ const debugMode = process.env.YTDL_MODE === 'debug';
let configPath = debugMode ? '../src/assets/default.json' : 'appdata/default.json';
var logger = null;
function setLogger(input_logger) { logger = input_logger; }
// https://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key
Object.byString = function(o, s) {
s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
@@ -51,7 +54,7 @@ function getConfigFile() {
let parsed_data = JSON.parse(raw_data);
return parsed_data;
} catch(e) {
console.log('ERROR: Failed to get config file');
logger.error('Failed to get config file');
return null;
}
}
@@ -68,10 +71,16 @@ function setConfigFile(config) {
function getConfigItem(key) {
let config_json = getConfigFile();
if (!CONFIG_ITEMS[key]) {
console.log('cannot find config with key ' + key);
logger.error(`Config item with key '${key}' is not recognized.`);
return null;
}
let path = CONFIG_ITEMS[key]['path'];
const val = Object.byString(config_json, path);
if (val === undefined && Object.byString(DEFAULT_CONFIG, path)) {
logger.warning(`Cannot find config with key '${key}'. Creating one with the default value...`);
setConfigItem(key, Object.byString(DEFAULT_CONFIG, path));
return Object.byString(DEFAULT_CONFIG, path);
}
return Object.byString(config_json, path);
};
@@ -124,7 +133,8 @@ module.exports = {
getConfigFile: getConfigFile,
setConfigFile: setConfigFile,
configExistsCheck: configExistsCheck,
CONFIG_ITEMS: CONFIG_ITEMS
CONFIG_ITEMS: CONFIG_ITEMS,
setLogger: setLogger
}
DEFAULT_CONFIG = {
@@ -153,6 +163,8 @@ DEFAULT_CONFIG = {
"settings_pin_required": false
},
"API": {
"use_API_key": false,
"API_key": "",
"use_youtube_API": false,
"youtube_API_key": ""
},

View File

@@ -68,6 +68,14 @@ let CONFIG_ITEMS = {
},
// API
'ytdl_use_api_key': {
'key': 'ytdl_use_api_key',
'path': 'YoutubeDLMaterial.API.use_API_key'
},
'ytdl_api_key': {
'key': 'ytdl_api_key',
'path': 'YoutubeDLMaterial.API.API_key'
},
'ytdl_use_youtube_api': {
'key': 'ytdl_use_youtube_api',
'path': 'YoutubeDLMaterial.API.use_youtube_API'
@@ -124,4 +132,7 @@ let CONFIG_ITEMS = {
},
};
module.exports.CONFIG_ITEMS = CONFIG_ITEMS;
module.exports = {
CONFIG_ITEMS: CONFIG_ITEMS,
CURRENT_VERSION: 'v3.6'
}

View File

@@ -5,7 +5,17 @@
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node app.js"
"start": "nodemon -q app.js"
},
"nodemonConfig": {
"ignore": [
"*.js",
"appdata/*",
"public/*"
],
"watch": [
"restart.json"
]
},
"repository": {
"type": "git",
@@ -24,13 +34,18 @@
"config": "^3.2.3",
"exe": "^1.0.2",
"express": "^4.17.1",
"fs-extra": "^9.0.0",
"lowdb": "^1.0.0",
"md5": "^2.2.1",
"node-id3": "^0.1.14",
"merge-files": "^0.1.2",
"node-fetch": "^2.6.0",
"node-id3": "^0.1.14",
"nodemon": "^2.0.2",
"progress": "^2.0.3",
"shortid": "^2.2.15",
"unzipper": "^0.10.10",
"uuidv4": "^6.0.6",
"winston": "^3.2.1",
"youtube-dl": "^3.0.2"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -106,13 +106,15 @@
"5fab47f146b0a4b809dcebf3db9da94df6299ea1": "Usar agente de descarga predeterminado",
"dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "Permitir descarga avanzada",
"52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Salvar",
"fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {Cerrar} false {Cancelar} other {Otro} }",
"cec82c0a545f37420d55a9b6c45c20546e82f94e": "Sobre YoutubeDL-Material",
"199c17e5d6a419313af3c325f06dcbb9645ca618": "es un descargador de código abierto de YouTube creado bajo las especificaciones de \"Material Design\" de Google. Puede descargar sin problemas sus videos favoritos como archivos de video o audio, e incluso suscribirse a sus canales favoritos y listas de reproducción para mantenerse actualizado con sus nuevos videos.",
"c072eebcb5b1f1eef6fb2ee1756e839dd302f3de": "tiene algunas características increíbles incluidas! Una amplia API, soporte de Docker y soporte de localización (traducción). Lea todas las funciones admitidas haciendo clic en el icono de GitHub a continuación.",
"bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "tiene algunas características increíbles incluidas! Una amplia API, soporte de Docker y soporte de localización (traducción). Lea todas las funciones compatibles haciendo clic en el icono de GitHub que se encuentra arriba.",
"a45e3b05f0529dc5246d70ef62304c94426d4c81": "Versión instalada:",
"e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Comprobando actualizaciones...",
"a16e92385b4fd9677bb830a4b796b8b79c113290": "Actualización disponible",
"b33536f59b94ec935a16bd6869d836895dc5300c": "¿Encontró un error o tiene una sugerencia?",
"e1f398f38ff1534303d4bb80bd6cece245f24016": "para crear una cuestión!",
"a45e3b05f0529dc5246d70ef62304c94426d4c81": "Versión instalada:",
"effdc7dfbbc49c08d25ea1748fca00c38c918abd": "Ver la última actualización",
"004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "Sobre",
"92eee6be6de0b11c924e3ab27db30257159c0a7c": "Inicio",
"5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f": "Guardar cambios",

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -11,8 +11,8 @@
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons|Material+Icons+Outlined" rel="stylesheet"/>
<link rel="stylesheet" href="styles.59c517d37ab207d8dcf9.css"></head>
<link rel="stylesheet" href="styles.5112d6db78cf21541598.css"></head>
<body>
<app-root></app-root>
<script src="runtime-es2015.49a45fb585e9064c0670.js" type="module"></script><script src="runtime-es5.49a45fb585e9064c0670.js" nomodule defer></script><script src="polyfills-es5.613e2d10a30e7a079e64.js" nomodule defer></script><script src="polyfills-es2015.7bc3b3aedaaa5e653ec6.js" type="module"></script><script src="main-es2015.51368ad793059fc3e28c.js" type="module"></script><script src="main-es5.51368ad793059fc3e28c.js" nomodule defer></script></body>
<script src="runtime-es2015.3e431b3a15319f824273.js" type="module"></script><script src="runtime-es5.3e431b3a15319f824273.js" nomodule defer></script><script src="polyfills-es5.613e2d10a30e7a079e64.js" nomodule defer></script><script src="polyfills-es2015.7bc3b3aedaaa5e653ec6.js" type="module"></script><script src="main-es2015.51368ad793059fc3e28c.js" type="module"></script><script src="main-es5.51368ad793059fc3e28c.js" nomodule defer></script></body>
</html>

View File

@@ -0,0 +1 @@
!function(e){function r(r){for(var n,a,i=r[0],c=r[1],l=r[2],p=0,s=[];p<i.length;p++)a=i[p],Object.prototype.hasOwnProperty.call(o,a)&&o[a]&&s.push(o[a][0]),o[a]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++)0!==o[t[i]]&&(n=!1);n&&(u.splice(r--,1),e=a(a.s=t[0]))}return e}var n={},o={0:0},u=[];function a(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,a),t.l=!0,t.exports}a.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,i=document.createElement("script");i.charset="utf-8",i.timeout=120,a.nc&&i.setAttribute("nonce",a.nc),i.src=function(e){return a.p+""+({}[e]||e)+"-es2015."+{1:"8a2b309101ca7894f706"}[e]+".js"}(e);var c=new Error;u=function(r){i.onerror=i.onload=null,clearTimeout(l);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:i})}),12e4);i.onerror=i.onload=u,document.head.appendChild(i)}return Promise.all(r)},a.m=e,a.c=n,a.d=function(e,r,t){a.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,r){if(1&r&&(e=a(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(a.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)a.d(t,n,(function(r){return e[r]}).bind(null,n));return t},a.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(r,"a",r),r},a.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},a.p="",a.oe=function(e){throw console.error(e),e};var i=window.webpackJsonp=window.webpackJsonp||[],c=i.push.bind(i);i.push=r,i=i.slice();for(var l=0;l<i.length;l++)r(i[l]);var f=c;t()}([]);

View File

@@ -1 +0,0 @@
!function(e){function r(r){for(var n,i,a=r[0],c=r[1],l=r[2],p=0,s=[];p<a.length;p++)i=a[p],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++)0!==o[t[a]]&&(n=!1);n&&(u.splice(r--,1),e=i(i.s=t[0]))}return e}var n={},o={0:0},u=[];function i(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,i),t.l=!0,t.exports}i.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,a=document.createElement("script");a.charset="utf-8",a.timeout=120,i.nc&&a.setAttribute("nonce",i.nc),a.src=function(e){return i.p+""+({}[e]||e)+"-es2015."+{1:"1ef01eeb5469c2dd4275"}[e]+".js"}(e);var c=new Error;u=function(r){a.onerror=a.onload=null,clearTimeout(l);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:a})}),12e4);a.onerror=a.onload=u,document.head.appendChild(a)}return Promise.all(r)},i.m=e,i.c=n,i.d=function(e,r,t){i.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,r){if(1&r&&(e=i(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(i.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)i.d(t,n,(function(r){return e[r]}).bind(null,n));return t},i.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(r,"a",r),r},i.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},i.p="",i.oe=function(e){throw console.error(e),e};var a=window.webpackJsonp=window.webpackJsonp||[],c=a.push.bind(a);a.push=r,a=a.slice();for(var l=0;l<a.length;l++)r(a[l]);var f=c;t()}([]);

View File

@@ -0,0 +1 @@
!function(e){function r(r){for(var n,a,i=r[0],c=r[1],l=r[2],p=0,s=[];p<i.length;p++)a=i[p],Object.prototype.hasOwnProperty.call(o,a)&&o[a]&&s.push(o[a][0]),o[a]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++)0!==o[t[i]]&&(n=!1);n&&(u.splice(r--,1),e=a(a.s=t[0]))}return e}var n={},o={0:0},u=[];function a(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,a),t.l=!0,t.exports}a.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,i=document.createElement("script");i.charset="utf-8",i.timeout=120,a.nc&&i.setAttribute("nonce",a.nc),i.src=function(e){return a.p+""+({}[e]||e)+"-es5."+{1:"8a2b309101ca7894f706"}[e]+".js"}(e);var c=new Error;u=function(r){i.onerror=i.onload=null,clearTimeout(l);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:i})}),12e4);i.onerror=i.onload=u,document.head.appendChild(i)}return Promise.all(r)},a.m=e,a.c=n,a.d=function(e,r,t){a.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,r){if(1&r&&(e=a(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(a.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)a.d(t,n,(function(r){return e[r]}).bind(null,n));return t},a.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(r,"a",r),r},a.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},a.p="",a.oe=function(e){throw console.error(e),e};var i=window.webpackJsonp=window.webpackJsonp||[],c=i.push.bind(i);i.push=r,i=i.slice();for(var l=0;l<i.length;l++)r(i[l]);var f=c;t()}([]);

View File

@@ -1 +0,0 @@
!function(e){function r(r){for(var n,i,a=r[0],c=r[1],l=r[2],p=0,s=[];p<a.length;p++)i=a[p],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,l||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++)0!==o[t[a]]&&(n=!1);n&&(u.splice(r--,1),e=i(i.s=t[0]))}return e}var n={},o={0:0},u=[];function i(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,i),t.l=!0,t.exports}i.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,a=document.createElement("script");a.charset="utf-8",a.timeout=120,i.nc&&a.setAttribute("nonce",i.nc),a.src=function(e){return i.p+""+({}[e]||e)+"-es5."+{1:"1ef01eeb5469c2dd4275"}[e]+".js"}(e);var c=new Error;u=function(r){a.onerror=a.onload=null,clearTimeout(l);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:a})}),12e4);a.onerror=a.onload=u,document.head.appendChild(a)}return Promise.all(r)},i.m=e,i.c=n,i.d=function(e,r,t){i.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,r){if(1&r&&(e=i(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(i.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)i.d(t,n,(function(r){return e[r]}).bind(null,n));return t},i.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(r,"a",r),r},i.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},i.p="",i.oe=function(e){throw console.error(e),e};var a=window.webpackJsonp=window.webpackJsonp||[],c=a.push.bind(a);a.push=r,a=a.slice();for(var l=0;l<a.length;l++)r(a[l]);var f=c;t()}([]);

View File

@@ -13,6 +13,9 @@ const db = low(adapter)
const debugMode = process.env.YTDL_MODE === 'debug';
var logger = null;
function setLogger(input_logger) { logger = input_logger; }
async function subscribe(sub) {
const result_obj = {
success: false,
@@ -23,7 +26,7 @@ async function subscribe(sub) {
sub.isPlaylist = sub.url.includes('playlist');
if (db.get('subscriptions').find({url: sub.url}).value()) {
console.log('Sub already exists');
logger.info('Sub already exists');
result_obj.error = 'Subcription with URL ' + sub.url + ' already exists!';
resolve(result_obj);
return;
@@ -48,14 +51,14 @@ async function getSubscriptionInfo(sub) {
let downloadConfig = ['--dump-json', '--playlist-end', '1']
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
if (debugMode) {
console.log('Subscribe: got info for subscription ' + sub.id);
logger.info('Subscribe: got info for subscription ' + sub.id);
}
if (err) {
console.log(err.stderr);
logger.error(err.stderr);
resolve(false);
} else if (output) {
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
if (debugMode) console.log('Could not get info for ' + sub.id);
logger.verbose('Could not get info for ' + sub.id);
resolve(false);
}
for (let i = 0; i < output.length; i++) {
@@ -184,6 +187,7 @@ async function getVideosForSub(sub) {
resolve(false);
return;
}
const sub_db = db.get('subscriptions').find({id: sub.id});
const basePath = config_api.getConfigItem('ytdl_subscriptions_base_path');
const useArchive = config_api.getConfigItem('ytdl_subscriptions_use_youtubedl_archive');
@@ -196,10 +200,6 @@ async function getVideosForSub(sub) {
let downloadConfig = ['-o', appendedBasePath + '/%(title)s.mp4', '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4', '-ciw', '--write-annotations', '--write-thumbnail', '--write-info-json', '--print-json'];
if (sub.timerange) {
downloadConfig.push('--dateafter', sub.timerange);
}
let archive_dir = null;
let archive_path = null;
@@ -211,17 +211,24 @@ async function getVideosForSub(sub) {
downloadConfig.push('--download-archive', archive_path);
}
// if streaming only mode, just get the list of videos
if (sub.streamingOnly) {
downloadConfig = ['-f', 'best', '--dump-json'];
}
if (sub.timerange) {
downloadConfig.push('--dateafter', sub.timerange);
}
// get videos
logger.verbose('Subscribe: getting videos for subscription ' + sub.name);
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
if (debugMode) {
console.log('Subscribe: got videos for subscription ' + sub.name);
}
if (err) {
console.log(err.stderr);
logger.error(err.stderr);
resolve(false);
} else if (output) {
if (output.length === 0 || (output.length === 1 && output[0] === '')) {
if (debugMode) console.log('No additional videos to download for ' + sub.name);
logger.verbose('No additional videos to download for ' + sub.name);
resolve(true);
}
for (let i = 0; i < output.length; i++) {
@@ -235,6 +242,18 @@ async function getVideosForSub(sub) {
continue;
}
if (sub.streamingOnly) {
if (i === 0) {
sub_db.assign({videos: []}).write();
}
// remove unnecessary info
output_json.formats = null;
// add to db
sub_db.get('videos').push(output_json).write();
}
// TODO: Potentially store downloaded files in db?
}
@@ -281,7 +300,7 @@ const deleteFolderRecursive = function(folder_to_delete) {
function removeIDFromArchive(archive_path, id) {
let data = fs.readFileSync(archive_path, {encoding: 'utf-8'});
if (!data) {
console.log('Archive could not be found.');
logger.error('Archive could not be found.');
return;
}
@@ -312,5 +331,6 @@ module.exports = {
unsubscribe : unsubscribe,
deleteSubscriptionFile : deleteSubscriptionFile,
getVideosForSub : getVideosForSub,
removeIDFromArchive : removeIDFromArchive
removeIDFromArchive : removeIDFromArchive,
setLogger : setLogger
}

View File

@@ -2,7 +2,6 @@
version: "2"
services:
ytdl_material:
build: .
environment:
ALLOW_CONFIG_MUTATIONS: 'true'
restart: always
@@ -13,4 +12,4 @@ services:
- ./subscriptions:/app/subscriptions
ports:
- "8998:17442"
image: tzahi12345/youtubedl-material:experimental
image: tzahi12345/youtubedl-material:latest

View File

@@ -1,6 +1,6 @@
{
"name": "youtube-dl-material",
"version": "3.5.0",
"version": "3.6",
"license": "MIT",
"scripts": {
"ng": "ng",
@@ -19,25 +19,21 @@
"private": true,
"dependencies": {
"@angular-devkit/core": "^9.0.6",
"@angular/animations": "^9.0.6",
"@angular/cdk": "^9.1.2",
"@angular/common": "^9.0.6",
"@angular/compiler": "^9.0.6",
"@angular/core": "^9.0.6",
"@angular/forms": "^9.0.6",
"@angular/http": "^7.2.15",
"@angular/localize": "^9.0.6",
"@angular/material": "^9.1.2",
"@angular/platform-browser": "^9.0.6",
"@angular/platform-browser-dynamic": "^9.0.6",
"@angular/router": "^9.0.6",
"@locl/core": "0.0.1-beta.2",
"@angular/animations": "^9.1.0",
"@angular/cdk": "^9.2.0",
"@angular/common": "^9.1.0",
"@angular/compiler": "^9.1.0",
"@angular/core": "^9.0.7",
"@angular/forms": "^9.1.0",
"@angular/localize": "^9.1.0",
"@angular/material": "^9.2.0",
"@angular/platform-browser": "^9.1.0",
"@angular/platform-browser-dynamic": "^9.1.0",
"@angular/router": "^9.1.0",
"core-js": "^2.4.1",
"file-saver": "^2.0.2",
"filesize": "^6.1.0",
"ng-lazyload-image": "^7.0.1",
"ng4-configure": "^0.1.7",
"ngx-content-loading": "^0.1.3",
"ngx-videogular": "^9.0.1",
"rxjs": "^6.5.3",
"rxjs-compat": "^6.0.0-rc.0",
@@ -47,11 +43,10 @@
"zone.js": "~0.10.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.900.6",
"@angular/cli": "^9.0.6",
"@angular/compiler-cli": "^9.0.6",
"@angular/language-service": "^9.0.6",
"@locl/cli": "0.0.1-beta.6",
"@angular-devkit/build-angular": "^0.901.0",
"@angular/cli": "^9.0.7",
"@angular/compiler-cli": "^9.0.7",
"@angular/language-service": "^9.0.7",
"@types/core-js": "^2.5.2",
"@types/file-saver": "^2.0.1",
"@types/jasmine": "2.5.45",

View File

@@ -16,6 +16,7 @@
top: 2px;
left: 10px;
position: relative;
pointer-events: none;
}
.sidenav-container {

View File

@@ -1,7 +1,7 @@
import { BrowserModule } from '@angular/platform-browser';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import { NgModule, LOCALE_ID } from '@angular/core';
import { registerLocaleData } from '@angular/common';
import { registerLocaleData, CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatCardModule } from '@angular/material/card';
@@ -24,10 +24,11 @@ import { MatSidenavModule } from '@angular/material/sidenav';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatToolbarModule } from '@angular/material/toolbar';
import {DragDropModule} from '@angular/cdk/drag-drop';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import { MatTabsModule } from '@angular/material/tabs';
import {DragDropModule} from '@angular/cdk/drag-drop';
import {ClipboardModule} from '@angular/cdk/clipboard';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import { AppComponent } from './app.component';
import { HttpModule } from '@angular/http';
import { HttpClientModule, HttpClient } from '@angular/common/http';
import { PostsService } from 'app/posts.services';
import { FileCardComponent } from './file-card/file-card.component';
@@ -38,7 +39,6 @@ import { PlayerComponent } from './player/player.component';
import {VgCoreModule, VgControlsModule, VgOverlayPlayModule, VgBufferingModule} from 'ngx-videogular';
import { InputDialogComponent } from './input-dialog/input-dialog.component';
import { LazyLoadImageModule, IsVisibleProps } from 'ng-lazyload-image';
import { NgxContentLoadingModule } from 'ngx-content-loading';
import { audioFilesMouseHovering, videoFilesMouseHovering, audioFilesOpened, videoFilesOpened } from './main/main.component';
import { CreatePlaylistComponent } from './create-playlist/create-playlist.component';
import { DownloadItemComponent } from './download-item/download-item.component';
@@ -54,6 +54,9 @@ import es from '@angular/common/locales/es';
import { AboutDialogComponent } from './dialogs/about-dialog/about-dialog.component';
import { VideoInfoDialogComponent } from './dialogs/video-info-dialog/video-info-dialog.component';
import { ArgModifierDialogComponent, HighlightPipe } from './dialogs/arg-modifier-dialog/arg-modifier-dialog.component';
import { UpdaterComponent } from './updater/updater.component';
import { UpdateProgressDialogComponent } from './dialogs/update-progress-dialog/update-progress-dialog.component';
import { ShareMediaDialogComponent } from './dialogs/share-media-dialog/share-media-dialog.component';
registerLocaleData(es, 'es');
export function isVisible({ event, element, scrollContainer, offset }: IsVisibleProps<any>) {
@@ -79,9 +82,13 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
AboutDialogComponent,
VideoInfoDialogComponent,
ArgModifierDialogComponent,
HighlightPipe
HighlightPipe,
UpdaterComponent,
UpdateProgressDialogComponent,
ShareMediaDialogComponent
],
imports: [
CommonModule,
BrowserModule,
BrowserAnimationsModule,
MatNativeDateModule,
@@ -90,7 +97,6 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
MatInputModule,
MatSelectModule,
ReactiveFormsModule,
HttpModule,
HttpClientModule,
MatToolbarModule,
MatCardModule,
@@ -111,14 +117,15 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible
MatSlideToggleModule,
MatMenuModule,
MatAutocompleteModule,
MatTabsModule,
MatTooltipModule,
DragDropModule,
ClipboardModule,
VgCoreModule,
VgControlsModule,
VgOverlayPlayModule,
VgBufferingModule,
LazyLoadImageModule.forRoot({ isVisible }),
NgxContentLoadingModule,
RouterModule,
AppRoutingModule,
],

1
src/app/consts.ts Normal file
View File

@@ -0,0 +1 @@
export const CURRENT_VERSION = 'v3.6';

View File

@@ -1,9 +1,11 @@
<h4 style="position: relative" mat-dialog-title i18n="About dialog title">About YoutubeDL-Material<span class="logo-image">
<a [href]="projectLink" target="_blank">
<img style="width: 32px;" src="assets/images/GitHub-64px.png">
</a>
<img style="width: 32px; margin-left: 15px;" src="assets/images/logo_128px.png">
</span></h4>
<h4 style="position: relative" mat-dialog-title><ng-container i18n="About dialog title">About YoutubeDL-Material</ng-container>
<span class="logo-image">
<a [href]="projectLink" target="_blank">
<img style="width: 32px;" src="assets/images/GitHub-64px.png">
</a>
<img style="width: 32px; margin-left: 15px;" src="assets/images/logo_128px.png">
</span>
</h4>
<mat-dialog-content>
<div style="margin-bottom: 5px;">
<p>
@@ -16,7 +18,7 @@
<h5 style="margin-top: 10px;">Installation details:</h5>
<p>
<ng-container i18n="Version label">Installed version:</ng-container>&nbsp;{{current_version_tag}} - <span style="display: inline-block" *ngIf="checking_for_updates"><mat-spinner class="version-spinner" [diameter]="22"></mat-spinner>&nbsp;<ng-container i18n="Checking for updates text">Checking for updates...</ng-container></span>
<mat-icon *ngIf="!checking_for_updates" class="version-checked-icon">done</mat-icon>&nbsp;&nbsp;<a *ngIf="!checking_for_updates && latestGithubRelease['tag_name'] !== current_version_tag" [href]="latestUpdateLink" target="_blank"><ng-container i18n="View latest update">Update available</ng-container> - {{latestGithubRelease['tag_name']}}</a>
<mat-icon *ngIf="!checking_for_updates" class="version-checked-icon">done</mat-icon>&nbsp;&nbsp;<a *ngIf="!checking_for_updates && latestGithubRelease['tag_name'] !== current_version_tag" [href]="latestUpdateLink" target="_blank"><ng-container i18n="View latest update">Update available</ng-container> - {{latestGithubRelease['tag_name']}}</a>. <ng-container i18n="Update through settings menu hint">You can update from the settings menu.</ng-container>
<span *ngIf="!checking_for_updates && latestGithubRelease['tag_name'] === current_version_tag">You are up to date.</span>
</p>
<p>

View File

@@ -1,5 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { PostsService } from 'app/posts.services';
import { CURRENT_VERSION } from 'app/consts';
@Component({
selector: 'app-about-dialog',
@@ -14,7 +15,7 @@ export class AboutDialogComponent implements OnInit {
latestGithubRelease = null;
checking_for_updates = true;
current_version_tag = 'v3.5';
current_version_tag = CURRENT_VERSION;
constructor(private postsService: PostsService) { }

View File

@@ -0,0 +1,25 @@
<h4 mat-dialog-title>
<ng-container *ngIf="is_playlist" i18n="Share playlist dialog title">Share playlist</ng-container>
<ng-container *ngIf="!is_playlist && type === 'video'" i18n="Share video dialog title">Share video</ng-container>
<ng-container *ngIf="!is_playlist && type === 'audio'" i18n="Share audio dialog title">Share audio</ng-container>
</h4>
<mat-dialog-content>
<div>
<div>
<mat-checkbox [checked]="sharing_enabled" (change)="sharingChanged($event)"><ng-container i18n="Enable sharing checkbox">Enable sharing</ng-container></mat-checkbox>
</div>
<div>
<mat-form-field style="width: 100%">
<input matInput [disabled]="!sharing_enabled" [readonly]="true" [value]="share_url">
</mat-form-field>
</div>
<div style="margin-bottom: 10px;">
<button color="accent" (click)="copiedToClipboard()" [disabled]="!sharing_enabled" [cdkCopyToClipboard]="share_url" mat-raised-button><ng-container i18n="Copy to clipboard button">Copy to clipboard</ng-container></button>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close><ng-container i18n="Close button">Close</ng-container></button>
</mat-dialog-actions>

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ShareMediaDialogComponent } from './share-media-dialog.component';
describe('ShareMediaDialogComponent', () => {
let component: ShareMediaDialogComponent;
let fixture: ComponentFixture<ShareMediaDialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ShareMediaDialogComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ShareMediaDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,71 @@
import { Component, OnInit, Inject } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { MatSnackBar } from '@angular/material/snack-bar';
import { PostsService } from 'app/posts.services';
@Component({
selector: 'app-share-media-dialog',
templateUrl: './share-media-dialog.component.html',
styleUrls: ['./share-media-dialog.component.scss']
})
export class ShareMediaDialogComponent implements OnInit {
type = null;
uid = null;
share_url = null;
sharing_enabled = null;
is_playlist = null;
constructor(@Inject(MAT_DIALOG_DATA) public data: any, public router: Router, private snackBar: MatSnackBar,
private postsService: PostsService) { }
ngOnInit(): void {
if (this.data) {
this.type = this.data.type;
this.uid = this.data.uid;
this.sharing_enabled = this.data.sharing_enabled;
this.is_playlist = this.data.is_playlist;
const arg = (this.is_playlist ? ';id=' : ';uid=');
this.share_url = window.location.href.split(';')[0] + arg + this.uid;
}
}
copiedToClipboard() {
this.openSnackBar('Copied to clipboard!');
}
sharingChanged(event) {
if (event.checked) {
this.postsService.enableSharing(this.uid, this.type, this.is_playlist).subscribe(res => {
if (res['success']) {
this.openSnackBar('Sharing enabled.');
this.sharing_enabled = true;
} else {
this.openSnackBar('Failed to enable sharing.');
}
}, err => {
this.openSnackBar('Failed to enable sharing - server error.');
});
} else {
this.postsService.disableSharing(this.uid, this.type, this.is_playlist).subscribe(res => {
if (res['success']) {
this.openSnackBar('Sharing disabled.');
this.sharing_enabled = false;
} else {
this.openSnackBar('Failed to disable sharing.');
}
}, err => {
this.openSnackBar('Failed to disable sharing - server error.');
});
}
}
public openSnackBar(message: string, action: string = '') {
this.snackBar.open(message, action, {
duration: 2000,
});
}
}

View File

@@ -29,6 +29,11 @@
</mat-option>
</mat-select>
</div>
<div class="col-12">
<div>
<mat-checkbox [(ngModel)]="streamingOnlyMode">Streaming-only mode</mat-checkbox>
</div>
</div>
</div>
</div>
</mat-dialog-content>
@@ -36,7 +41,7 @@
<mat-dialog-actions>
<button mat-button mat-dialog-close><ng-container i18n="Subscribe cancel button">Cancel</ng-container></button>
<!-- The mat-dialog-close directive optionally accepts a value as a result for the dialog. -->
<button mat-button [disabled]="!url" type="submit" (click)="subscribeClicked()">Subscribe</button>
<button mat-button [disabled]="!url" type="submit" (click)="subscribeClicked()"><ng-container i18n="Subscribe button">Subscribe</ng-container></button>
<div class="mat-spinner" *ngIf="subscribing">
<mat-spinner [diameter]="25"></mat-spinner>
</div>

View File

@@ -19,6 +19,9 @@ export class SubscribeDialogComponent implements OnInit {
// state
subscribing = false;
// no videos actually downloaded, just streamed
streamingOnlyMode = false;
time_units = [
'day',
'week',
@@ -46,8 +49,7 @@ export class SubscribeDialogComponent implements OnInit {
if (!this.download_all) {
timerange = 'now-' + this.timerange_amount.toString() + this.timerange_unit;
}
this.postsService.createSubscription(this.url, this.name, timerange).subscribe(res => {
this.postsService.createSubscription(this.url, this.name, timerange, this.streamingOnlyMode).subscribe(res => {
this.subscribing = false;
if (res['new_sub']) {
this.dialogRef.close(res['new_sub']);

View File

@@ -0,0 +1,18 @@
<h4 i18n="Update progress dialog title" mat-dialog-title>Updater</h4>
<mat-dialog-content>
<div *ngIf="updateStatus">
<div style="margin-bottom: 8px;">
<h6 *ngIf="updateStatus['updating']">Update in progress</h6>
<h6 *ngIf="!updateStatus['updating'] && updateStatus['error']">Update failed</h6>
<h6 *ngIf="!updateStatus['updating'] && !updateStatus['error']">Update succeeded!</h6>
</div>
<mat-progress-bar *ngIf="updateStatus['updating']" mode="indeterminate"></mat-progress-bar>
<mat-progress-bar *ngIf="!updateStatus['updating']" mode="determinate" value="100"></mat-progress-bar>
<p style="margin-top: 4px; font-size: 13px;" *ngIf="updateStatus['details']">{{updateStatus['details']}}</p>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close><ng-container i18n="Close update progress dialog">Close</ng-container></button>
</mat-dialog-actions>

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { UpdateProgressDialogComponent } from './update-progress-dialog.component';
describe('UpdateProgressDialogComponent', () => {
let component: UpdateProgressDialogComponent;
let fixture: ComponentFixture<UpdateProgressDialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ UpdateProgressDialogComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(UpdateProgressDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,42 @@
import { Component, OnInit } from '@angular/core';
import { PostsService } from 'app/posts.services';
import { MatSnackBar } from '@angular/material/snack-bar';
@Component({
selector: 'app-update-progress-dialog',
templateUrl: './update-progress-dialog.component.html',
styleUrls: ['./update-progress-dialog.component.scss']
})
export class UpdateProgressDialogComponent implements OnInit {
updateStatus = null;
updateInterval = 250;
errored = false;
constructor(private postsService: PostsService, private snackBar: MatSnackBar) { }
ngOnInit(): void {
this.getUpdateProgress();
setInterval(() => {
if (this.updateStatus['updating']) { this.getUpdateProgress(); }
}, 250);
}
getUpdateProgress() {
this.postsService.getUpdaterStatus().subscribe(res => {
if (res) {
this.updateStatus = res;
if (this.updateStatus && this.updateStatus['error']) {
this.openSnackBar('Update failed. Check logs for more details.');
}
}
});
}
public openSnackBar(message: string, action: string = '') {
this.snackBar.open(message, action, {
duration: 2000,
});
}
}

View File

@@ -15,15 +15,15 @@
</div>
<div class="info-item">
<div class="info-item-label"><strong><ng-container i18n="Video file size property">File size:</ng-container>&nbsp;</strong></div>
<div class="info-item-value">{{filesize(file.size)}}</div>
<div class="info-item-value">{{file.size ? filesize(file.size) : 'N/A'}}</div>
</div>
<div class="info-item">
<div class="info-item-label"><strong><ng-container i18n="Video path property">Path:</ng-container>&nbsp;</strong></div>
<div class="info-item-value">{{file.path}}</div>
<div class="info-item-value">{{file.path ? file.path : 'N/A'}}</div>
</div>
<div class="info-item">
<div class="info-item-label"><strong><ng-container i18n="Video upload date property">Upload Date:</ng-container>&nbsp;</strong></div>
<div class="info-item-value">{{file.upload_date}}</div>
<div class="info-item-value">{{file.upload_date ? file.upload_date : 'N/A'}}</div>
</div>
</mat-dialog-content>

View File

@@ -16,3 +16,7 @@
width: 30%;
vertical-align: top;
}
.a-wrap {
word-wrap: break-word
}

View File

@@ -2,7 +2,7 @@
<div style="padding:5px">
<div style="height: 52px;">
<div>
<b><a class="file-link" href="javascript:void(0)" (click)="!isPlaylist ? mainComponent.goToFile(name, isAudio) : mainComponent.goToPlaylist(name, type)">{{title}}</a></b>
<b><a class="file-link" href="javascript:void(0)" (click)="!isPlaylist ? mainComponent.goToFile(name, isAudio, uid) : mainComponent.goToPlaylist(name, type)">{{title}}</a></b>
</div>
<span class="max-two-lines"><ng-container i18n="File or playlist ID">ID:</ng-container>&nbsp;{{name}}</span>
<div *ngIf="isPlaylist"><ng-container i18n="Playlist video count">Count:</ng-container>&nbsp;{{count}}</div>
@@ -10,9 +10,7 @@
<div *ngIf="!image_errored && thumbnailURL" class="img-div">
<img class="image" (error) ="onImgError($event)" [id]="type" [lazyLoad]="thumbnailURL" [customObservable]="scrollAndLoad" (onLoad)="imageLoaded($event)" alt="Thumbnail">
<span *ngIf="!image_loaded">
<ngx-content-loading [width]="500" [height]="360">
<svg:g ngx-rect width="500" height="360" y="0" x="0" rx="4" ry="4"></svg:g>
</ngx-content-loading>
</span>
</div>
</div>

View File

@@ -18,6 +18,7 @@ export class FileCardComponent implements OnInit {
@Input() title: string;
@Input() length: string;
@Input() name: string;
@Input() uid: string;
@Input() thumbnailURL: string;
@Input() isAudio = true;
@Output() removeFile: EventEmitter<string> = new EventEmitter<string>();
@@ -47,13 +48,15 @@ export class FileCardComponent implements OnInit {
deleteFile(blacklistMode = false) {
if (!this.isPlaylist) {
this.postsService.deleteFile(this.name, this.isAudio, blacklistMode).subscribe(result => {
if (result === true) {
this.postsService.deleteFile(this.uid, this.isAudio, blacklistMode).subscribe(result => {
if (result) {
this.openSnackBar('Delete success!', 'OK.');
this.removeFile.emit(this.name);
} else {
this.openSnackBar('Delete failed!', 'OK.');
}
}, err => {
this.openSnackBar('Delete failed!', 'OK.');
});
} else {
this.removeFile.emit(this.name);

View File

@@ -204,7 +204,7 @@
<div *ngIf="mp3s.length > 0;else nomp3s">
<mat-grid-list style="margin-bottom: 15px;" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
<mat-grid-tile *ngFor="let file of mp3s; index as i;">
<app-file-card #audiofilecard (removeFile)="removeFromMp3($event)" [file]="file" [title]="file.title" [name]="file.id" [thumbnailURL]="file.thumbnailURL"
<app-file-card #audiofilecard (removeFile)="removeFromMp3($event)" [file]="file" [title]="file.title" [name]="file.id" [uid]="file.uid" [thumbnailURL]="file.thumbnailURL"
[length]="file.duration" [isAudio]="true" [use_youtubedl_archive]="use_youtubedl_archive"></app-file-card>
<mat-progress-bar *ngIf="downloading_content['audio'][file.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
</mat-grid-tile>
@@ -245,7 +245,7 @@
<div *ngIf="mp4s.length > 0;else nomp4s">
<mat-grid-list style="margin-bottom: 15px;" (window:resize)="onResize($event)" [cols]="files_cols" rowHeight="150px">
<mat-grid-tile *ngFor="let file of mp4s; index as i;">
<app-file-card #videofilecard (removeFile)="removeFromMp4($event)" [file]="file" [title]="file.title" [name]="file.id" [thumbnailURL]="file.thumbnailURL"
<app-file-card #videofilecard (removeFile)="removeFromMp4($event)" [file]="file" [title]="file.title" [name]="file.id" [uid]="file.uid" [thumbnailURL]="file.thumbnailURL"
[length]="file.duration" [isAudio]="false" [use_youtubedl_archive]="use_youtubedl_archive"></app-file-card>
<mat-progress-bar *ngIf="downloading_content['video'][file.id]" class="download-progress-bar" mode="indeterminate"></mat-progress-bar>
</mat-grid-tile>

View File

@@ -390,11 +390,11 @@ export class MainComponent implements OnInit {
}
}
public goToFile(name, isAudio) {
public goToFile(name, isAudio, uid) {
if (isAudio) {
this.downloadHelperMp3(name, false, false);
this.downloadHelperMp3(name, uid, false, false);
} else {
this.downloadHelperMp4(name, false, false);
this.downloadHelperMp4(name, uid, false, false);
}
}
@@ -407,7 +407,7 @@ export class MainComponent implements OnInit {
} else {
localStorage.setItem('player_navigator', this.router.url);
const fileNames = playlist.fileNames;
this.router.navigate(['/player', {fileNames: fileNames.join('|nvr|'), type: type, id: playlistID}]);
this.router.navigate(['/player', {fileNames: fileNames.join('|nvr|'), type: type, id: playlistID, uid: playlistID}]);
}
} else {
// playlist not found
@@ -463,7 +463,7 @@ export class MainComponent implements OnInit {
// download helpers
downloadHelperMp3(name, is_playlist = false, forceView = false, new_download = null) {
downloadHelperMp3(name, uid, is_playlist = false, forceView = false, new_download = null) {
this.downloadingfile = false;
if (this.multiDownloadMode && !this.downloadOnlyMode) {
@@ -482,7 +482,7 @@ export class MainComponent implements OnInit {
if (is_playlist) {
this.router.navigate(['/player', {fileNames: name.join('|nvr|'), type: 'audio'}]);
} else {
this.router.navigate(['/player', {fileNames: name, type: 'audio'}]);
this.router.navigate(['/player', {fileNames: name, type: 'audio', uid: uid}]);
}
}
}
@@ -501,7 +501,7 @@ export class MainComponent implements OnInit {
}
}
downloadHelperMp4(name, is_playlist = false, forceView = false, new_download = null) {
downloadHelperMp4(name, uid, is_playlist = false, forceView = false, new_download = null) {
this.downloadingfile = false;
if (this.multiDownloadMode && !this.downloadOnlyMode) {
// do nothing
@@ -519,7 +519,7 @@ export class MainComponent implements OnInit {
if (is_playlist) {
this.router.navigate(['/player', {fileNames: name.join('|nvr|'), type: 'video'}]);
} else {
this.router.navigate(['/player', {fileNames: name, type: 'video'}]);
this.router.navigate(['/player', {fileNames: name, type: 'video', uid: uid}]);
}
}
}
@@ -592,7 +592,7 @@ export class MainComponent implements OnInit {
this.path = is_playlist ? posts['file_names'] : posts['audiopathEncoded'];
if (this.path !== '-1') {
this.downloadHelperMp3(this.path, is_playlist, false, new_download);
this.downloadHelperMp3(this.path, posts['uid'], is_playlist, false, new_download);
}
}, error => { // can't access server or failed to download for other reasons
this.downloadingfile = false;
@@ -631,7 +631,7 @@ export class MainComponent implements OnInit {
this.path = is_playlist ? posts['file_names'] : posts['videopathEncoded'];
if (this.path !== '-1') {
this.downloadHelperMp4(this.path, is_playlist, false, new_download);
this.downloadHelperMp4(this.path, posts['uid'], is_playlist, false, new_download);
}
}, error => { // can't access server
this.downloadingfile = false;
@@ -717,7 +717,7 @@ export class MainComponent implements OnInit {
this.postsService.downloadFileFromServer(name, 'audio').subscribe(res => {
this.downloading_content['audio'][name] = false;
const blob: Blob = res;
saveAs(blob, name + '.mp3');
saveAs(blob, decodeURIComponent(name) + '.mp3');
if (!this.fileManagerEnabled) {
// tell server to delete the file once downloaded
@@ -734,7 +734,7 @@ export class MainComponent implements OnInit {
this.postsService.downloadFileFromServer(name, 'video').subscribe(res => {
this.downloading_content['video'][name] = false;
const blob: Blob = res;
saveAs(blob, name + '.mp4');
saveAs(blob, decodeURIComponent(name) + '.mp4');
if (!this.fileManagerEnabled) {
// tell server to delete the file once downloaded

View File

@@ -45,13 +45,19 @@
.save-button {
right: 25px;
position: absolute;
position: fixed;
bottom: 25px;
}
.share-button {
left: 25px;
position: fixed;
bottom: 25px;
}
.favorite-button {
left: 25px;
position: absolute;
position: fixed;
bottom: 25px;
}

View File

@@ -1,4 +1,4 @@
<div *ngIf="playlist.length > 0">
<div *ngIf="playlist.length > 0 && show_player">
<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">
@@ -26,8 +26,10 @@
<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>
<button *ngIf="!is_shared && id" class="share-button" color="primary" (click)="openShareDialog()" mat-fab><mat-icon class="save-icon">share</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>
<button *ngIf="!is_shared && type !== 'subscription'" class="share-button" color="primary" (click)="openShareDialog()" mat-fab><mat-icon class="save-icon">share</mat-icon></button>
</div>
</div>

View File

@@ -6,6 +6,7 @@ import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { InputDialogComponent } from 'app/input-dialog/input-dialog.component';
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { ShareMediaDialogComponent } from '../dialogs/share-media-dialog/share-media-dialog.component';
export interface IMedia {
title: string;
@@ -25,6 +26,8 @@ export class PlayerComponent implements OnInit {
original_playlist: string = null;
playlist_updating = false;
show_player = false;
currentIndex = 0;
currentItem: IMedia = null;
api: VgAPI;
@@ -33,14 +36,24 @@ export class PlayerComponent implements OnInit {
fileNames: string[];
type: string;
id = null; // used for playlists (not subscription)
uid = null; // used for non-subscription files (audio, video, playlist)
subscriptionName = null;
subPlaylist = null;
is_shared = false;
db_playlist = null;
db_file = null;
baseStreamPath = null;
audioFolderPath = null;
videoFolderPath = null;
subscriptionFolderPath = null;
// url-mode params
url = null;
name = null;
innerWidth: number;
downloading = false;
@@ -53,11 +66,13 @@ export class PlayerComponent implements OnInit {
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');
this.uid = this.route.snapshot.paramMap.get('uid');
this.subscriptionName = this.route.snapshot.paramMap.get('subscriptionName');
this.subPlaylist = this.route.snapshot.paramMap.get('subPlaylist');
this.url = this.route.snapshot.paramMap.get('url');
this.name = this.route.snapshot.paramMap.get('name');
// loading config
this.postsService.loadNavItems().subscribe(res => { // loads settings
@@ -66,53 +81,35 @@ export class PlayerComponent implements OnInit {
this.audioFolderPath = result['YoutubeDLMaterial']['Downloader']['path-audio'];
this.videoFolderPath = result['YoutubeDLMaterial']['Downloader']['path-video'];
this.subscriptionFolderPath = result['YoutubeDLMaterial']['Subscriptions']['subscriptions_base_path'];
this.fileNames = this.route.snapshot.paramMap.get('fileNames') ? this.route.snapshot.paramMap.get('fileNames').split('|nvr|') : null;
let fileType = null;
if (this.type === 'audio') {
fileType = 'audio/mp3';
} else if (this.type === 'video') {
fileType = 'video/mp4';
} else if (this.type === 'subscription') {
// only supports mp4 for now
fileType = 'video/mp4';
} else {
// error
console.error('Must have valid file type! Use \'audio\', \'video\', or \'subscription\'.');
if (!this.fileNames) {
this.is_shared = true;
}
for (let i = 0; i < this.fileNames.length; i++) {
const fileName = this.fileNames[i];
let baseLocation = null;
let fullLocation = null;
if (!this.subscriptionName) {
baseLocation = this.type + '/';
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName);
} else {
// default to video but include subscription name param
baseLocation = 'video/';
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName) + '?subName=' + this.subscriptionName +
'&subPlaylist=' + this.subPlaylist;
}
// if it has a slash (meaning it's in a directory), only get the file name for the label
let label = null;
const decodedName = decodeURIComponent(fileName);
const hasSlash = decodedName.includes('/') || decodedName.includes('\\');
if (hasSlash) {
label = decodedName.replace(/^.*[\\\/]/, '');
} else {
label = decodedName;
}
const mediaObject: IMedia = {
title: fileName,
src: fullLocation,
type: fileType,
label: label
}
this.playlist.push(mediaObject);
if (this.uid && !this.id) {
this.getFile();
} else if (this.id) {
this.getPlaylistFiles();
}
if (this.url) {
// if a url is given, just stream the URL
this.playlist = [];
const imedia: IMedia = {
title: this.name,
label: this.name,
src: this.url,
type: 'video/mp4'
}
this.playlist.push(imedia);
this.currentItem = this.playlist[0];
this.currentIndex = 0;
this.show_player = true;
} else if (this.type === 'subscription' || this.fileNames) {
this.show_player = true;
this.parseFileNames();
}
this.currentItem = this.playlist[this.currentIndex];
this.original_playlist = JSON.stringify(this.playlist);
});
// this.getFileInfos();
@@ -124,6 +121,85 @@ export class PlayerComponent implements OnInit {
}
getFile() {
const already_has_filenames = !!this.fileNames;
this.postsService.getFile(this.uid, null).subscribe(res => {
this.db_file = res['file'];
if (!this.fileNames) {
// means it's a shared video
if (!this.id) {
// regular video/audio file (not playlist)
this.fileNames = [this.db_file['id']];
this.type = this.db_file['isAudio'] ? 'audio' : 'video';
if (!already_has_filenames) { this.parseFileNames(); }
}
}
if (this.db_file['sharingEnabled']) {
this.show_player = true;
} else if (!already_has_filenames) {
this.openSnackBar('Error: Sharing has been disabled for this video!', 'Dismiss');
}
});
}
getPlaylistFiles() {
this.postsService.getPlaylist(this.id, null).subscribe(res => {
this.db_playlist = res['playlist'];
this.fileNames = this.db_playlist['fileNames'];
this.type = res['type'];
this.show_player = true;
this.parseFileNames();
});
}
parseFileNames() {
let fileType = null;
if (this.type === 'audio') {
fileType = 'audio/mp3';
} else if (this.type === 'video') {
fileType = 'video/mp4';
} else if (this.type === 'subscription') {
// only supports mp4 for now
fileType = 'video/mp4';
} else {
// error
console.error('Must have valid file type! Use \'audio\', \'video\', or \'subscription\'.');
}
this.playlist = [];
for (let i = 0; i < this.fileNames.length; i++) {
const fileName = this.fileNames[i];
let baseLocation = null;
let fullLocation = null;
if (!this.subscriptionName) {
baseLocation = this.type + '/';
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName);
} else {
// default to video but include subscription name param
baseLocation = 'video/';
fullLocation = this.baseStreamPath + baseLocation + encodeURIComponent(fileName) + '?subName=' + this.subscriptionName +
'&subPlaylist=' + this.subPlaylist;
}
// if it has a slash (meaning it's in a directory), only get the file name for the label
let label = null;
const decodedName = decodeURIComponent(fileName);
const hasSlash = decodedName.includes('/') || decodedName.includes('\\');
if (hasSlash) {
label = decodedName.replace(/^.*[\\\/]/, '');
} else {
label = decodedName;
}
const mediaObject: IMedia = {
title: fileName,
src: fullLocation,
type: fileType,
label: label
}
this.playlist.push(mediaObject);
}
this.currentItem = this.playlist[this.currentIndex];
this.original_playlist = JSON.stringify(this.playlist);
}
onPlayerReady(api: VgAPI) {
this.api = api;
@@ -193,7 +269,7 @@ export class PlayerComponent implements OnInit {
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.postsService.downloadFileFromServer(filename, this.type, null, null, this.subscriptionName, this.subPlaylist).subscribe(res => {
this.downloading = false;
const blob: Blob = res;
saveAs(blob, filename + ext);
@@ -224,6 +300,7 @@ export class PlayerComponent implements OnInit {
if (res['success']) {
dialogRef.close();
const new_playlist = res['new_playlist'];
this.db_playlist = new_playlist;
this.openSnackBar('Playlist \'' + name + '\' successfully created!', '')
this.playlistPostCreationHandler(new_playlist.id);
}
@@ -273,6 +350,26 @@ export class PlayerComponent implements OnInit {
})
}
openShareDialog() {
const dialogRef = this.dialog.open(ShareMediaDialogComponent, {
data: {
uid: this.id ? this.id : this.uid,
type: this.type,
sharing_enabled: this.id ? this.db_playlist.sharingEnabled : this.db_file.sharingEnabled,
is_playlist: !!this.id
},
width: '60vw'
});
dialogRef.afterClosed().subscribe(res => {
if (!this.id) {
this.getFile();
} else {
this.getPlaylistFiles();
}
});
}
// snackbar helper
public openSnackBar(message: string, action: string) {
this.snackBar.open(message, action, {

View File

@@ -1,8 +1,6 @@
import {Injectable, isDevMode, Inject} from '@angular/core';
import { HttpClient, HttpHeaders, HttpRequest, HttpResponseBase } from '@angular/common/http';
import config from '../assets/default.json';
import { HttpClient, HttpHeaders } from '@angular/common/http';
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';
@@ -22,6 +20,8 @@ export class PostsService {
THEMES_CONFIG = THEMES_CONFIG;
theme;
settings_changed = new BehaviorSubject<boolean>(false);
auth_token = '4241b401-7236-493e-92b5-b72696b9d853';
httpOptions = null;
debugMode = false;
constructor(private http: HttpClient, private router: Router, @Inject(DOCUMENT) private document: Document) {
@@ -34,6 +34,12 @@ export class PostsService {
this.debugMode = true;
this.path = 'http://localhost:17442/api/';
}
this.httpOptions = {
headers: new HttpHeaders({
'Authorization': '4241b401-7236-493e-92b5-b72696b9d853'
}),
};
}
setTheme(theme) {
@@ -64,7 +70,7 @@ export class PostsService {
customArgs: customArgs,
customOutput: customOutput,
youtubeUsername: youtubeUsername,
youtubePassword: youtubePassword});
youtubePassword: youtubePassword}, this.httpOptions);
}
// tslint:disable-next-line: max-line-length
@@ -75,116 +81,156 @@ export class PostsService {
customArgs: customArgs,
customOutput: customOutput,
youtubeUsername: youtubeUsername,
youtubePassword: youtubePassword});
youtubePassword: youtubePassword}, this.httpOptions);
}
getFileStatusMp3(name: string) {
return this.http.post(this.path + 'fileStatusMp3', {name: name});
return this.http.post(this.path + 'fileStatusMp3', {name: name}, this.httpOptions);
}
getFileStatusMp4(name: string) {
return this.http.post(this.path + 'fileStatusMp4', {name: name});
return this.http.post(this.path + 'fileStatusMp4', {name: name}, this.httpOptions);
}
loadNavItems() {
if (isDevMode()) {
return this.http.get('./assets/default.json');
} else {
return this.http.get(this.path + 'config');
return this.http.get(this.path + 'config', this.httpOptions);
}
}
setConfig(config) {
return this.http.post(this.path + 'setConfig', {new_config_file: config});
loadAsset(name) {
return this.http.get(`./assets/${name}`);
}
deleteFile(name: string, isAudio: boolean, blacklistMode = false) {
setConfig(config) {
return this.http.post(this.path + 'setConfig', {new_config_file: config}, this.httpOptions);
}
deleteFile(uid: string, isAudio: boolean, blacklistMode = false) {
if (isAudio) {
return this.http.post(this.path + 'deleteMp3', {name: name, blacklistMode: blacklistMode});
return this.http.post(this.path + 'deleteMp3', {uid: uid, blacklistMode: blacklistMode}, this.httpOptions);
} else {
return this.http.post(this.path + 'deleteMp4', {name: name, blacklistMode: blacklistMode});
return this.http.post(this.path + 'deleteMp4', {uid: uid, blacklistMode: blacklistMode}, this.httpOptions);
}
}
getMp3s() {
return this.http.post(this.path + 'getMp3s', {});
return this.http.get(this.path + 'getMp3s', this.httpOptions);
}
getMp4s() {
return this.http.post(this.path + 'getMp4s', {});
return this.http.get(this.path + 'getMp4s', this.httpOptions);
}
downloadFileFromServer(fileName, type, outputName = null, fullPathProvided = null) {
getFile(uid, type) {
return this.http.post(this.path + 'getFile', {uid: uid, type: type}, this.httpOptions);
}
downloadFileFromServer(fileName, type, outputName = null, fullPathProvided = null, subscriptionName = null, subPlaylist = null) {
return this.http.post(this.path + 'downloadFile', {fileNames: fileName,
type: type,
zip_mode: Array.isArray(fileName),
outputName: outputName,
fullPathProvided: fullPathProvided},
{responseType: 'blob'});
fullPathProvided: fullPathProvided,
subscriptionName: subscriptionName,
subPlaylist: subPlaylist
},
{responseType: 'blob', headers: this.httpOptions.headers});
}
downloadArchive(sub) {
return this.http.post(this.path + 'downloadArchive', {sub: sub}, {responseType: 'blob'});
return this.http.post(this.path + 'downloadArchive', {sub: sub}, {responseType: 'blob', headers: this.httpOptions.headers});
}
getFileInfo(fileNames, type, urlMode) {
return this.http.post(this.path + 'getVideoInfos', {fileNames: fileNames, type: type, urlMode: urlMode});
return this.http.post(this.path + 'getVideoInfos', {fileNames: fileNames, type: type, urlMode: urlMode}, this.httpOptions);
}
isPinSet() {
return this.http.post(this.path + 'isPinSet', {});
return this.http.post(this.path + 'isPinSet', {}, this.httpOptions);
}
setPin(unhashed_pin) {
return this.http.post(this.path + 'setPin', {pin: unhashed_pin});
return this.http.post(this.path + 'setPin', {pin: unhashed_pin}, this.httpOptions);
}
checkPin(unhashed_pin) {
return this.http.post(this.path + 'checkPin', {input_pin: unhashed_pin});
return this.http.post(this.path + 'checkPin', {input_pin: unhashed_pin}, this.httpOptions);
}
generateNewAPIKey() {
return this.http.post(this.path + 'generateNewAPIKey', {}, this.httpOptions);
}
enableSharing(uid, type, is_playlist) {
return this.http.post(this.path + 'enableSharing', {uid: uid, type: type, is_playlist: is_playlist}, this.httpOptions);
}
disableSharing(uid, type, is_playlist) {
return this.http.post(this.path + 'disableSharing', {uid: uid, type: type, is_playlist: is_playlist}, this.httpOptions);
}
createPlaylist(playlistName, fileNames, type, thumbnailURL) {
return this.http.post(this.path + 'createPlaylist', {playlistName: playlistName,
fileNames: fileNames,
type: type,
thumbnailURL: thumbnailURL});
thumbnailURL: thumbnailURL}, this.httpOptions);
}
getPlaylist(playlistID, type) {
return this.http.post(this.path + 'getPlaylist', {playlistID: playlistID,
type: type}, this.httpOptions);
}
updatePlaylist(playlistID, fileNames, type) {
return this.http.post(this.path + 'updatePlaylist', {playlistID: playlistID,
fileNames: fileNames,
type: type});
type: type}, this.httpOptions);
}
removePlaylist(playlistID, type) {
return this.http.post(this.path + 'deletePlaylist', {playlistID: playlistID, type: type});
return this.http.post(this.path + 'deletePlaylist', {playlistID: playlistID, type: type}, this.httpOptions);
}
createSubscription(url, name, timerange = null) {
return this.http.post(this.path + 'subscribe', {url: url, name: name, timerange: timerange})
createSubscription(url, name, timerange = null, streamingOnly = false) {
return this.http.post(this.path + 'subscribe', {url: url, name: name, timerange: timerange, streamingOnly: streamingOnly},
this.httpOptions);
}
unsubscribe(sub, deleteMode = false) {
return this.http.post(this.path + 'unsubscribe', {sub: sub, deleteMode: deleteMode})
return this.http.post(this.path + 'unsubscribe', {sub: sub, deleteMode: deleteMode}, this.httpOptions)
}
deleteSubscriptionFile(sub, file, deleteForever) {
return this.http.post(this.path + 'deleteSubscriptionFile', {sub: sub, file: file, deleteForever: deleteForever})
return this.http.post(this.path + 'deleteSubscriptionFile', {sub: sub, file: file, deleteForever: deleteForever}, this.httpOptions)
}
getSubscription(id) {
return this.http.post(this.path + 'getSubscription', {id: id});
return this.http.post(this.path + 'getSubscription', {id: id}, this.httpOptions);
}
getAllSubscriptions() {
return this.http.post(this.path + 'getAllSubscriptions', {});
return this.http.post(this.path + 'getAllSubscriptions', {}, this.httpOptions);
}
// updates the server to the latest version
updateServer(tag) {
return this.http.post(this.path + 'updateServer', {tag: tag}, this.httpOptions);
}
getUpdaterStatus() {
return this.http.get(this.path + 'updaterStatus', this.httpOptions);
}
// gets tag of the latest version of youtubedl-material
getLatestGithubRelease() {
return this.http.get('https://api.github.com/repos/tzahi12345/youtubedl-material/releases/latest');
}
getAvailableRelease() {
return this.http.get('https://api.github.com/repos/tzahi12345/youtubedl-material/releases');
}
}

View File

@@ -1,283 +1,269 @@
<h4 i18n="Settings title" mat-dialog-title>Settings</h4>
<!-- <ng-container i18n="Allow subscriptions setting"></ng-container> -->
<mat-dialog-content>
<!-- Language -->
<!-- Language
<div style="margin-bottom: 10px;">
Language:&nbsp;&nbsp;&nbsp;<mat-select class="locale-select" (selectionChange)="localeSelectChanged($event.value)" [(value)]="initialLocale">
<mat-option *ngFor="let locale of supported_locales" [value]="locale">
{{all_locales[locale]['nativeName']}}
</mat-option>
</mat-select>
</div>
</div> -->
<!-- Host -->
<mat-expansion-panel class="settings-expansion-panel mat-elevation-z4">
<mat-expansion-panel-header>
<mat-panel-title>
<ng-container i18n="Host settings title">Host</ng-container>
</mat-panel-title>
</mat-expansion-panel-header>
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12">
<mat-form-field color="accent">
<input [(ngModel)]="new_config['Host']['url']" matInput placeholder="URL" i18n-placeholder="URL input placeholder" required>
<mat-hint><ng-container i18n="URL setting input hint">URL this app will be accessed from, without the port.</ng-container></mat-hint>
</mat-form-field>
<mat-tab-group>
<!-- Server -->
<mat-tab label="Main" i18n-label="Main settings label">
<ng-template matTabContent style="padding: 15px;">
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12 mt-3">
<mat-form-field color="accent">
<input [(ngModel)]="new_config['Host']['url']" matInput placeholder="URL" i18n-placeholder="URL input placeholder" required>
<mat-hint><ng-container i18n="URL setting input hint">URL this app will be accessed from, without the port.</ng-container></mat-hint>
</mat-form-field>
</div>
<div class="col-12 mb-4">
<mat-form-field color="accent">
<input [(ngModel)]="new_config['Host']['port']" matInput placeholder="Port" i18n-placeholder="Port input placeholder" required>
<mat-hint><ng-container i18n="Port setting input hint">The desired port. Default is 17442.</ng-container></mat-hint>
</mat-form-field>
</div>
</div>
</div>
<div class="col-12 mt-4">
<mat-form-field color="accent">
<input [(ngModel)]="new_config['Host']['port']" matInput placeholder="Port" i18n-placeholder="Port input placeholder" required>
<mat-hint><ng-container i18n="Port setting input hint">The desired port. Default is 17442.</ng-container></mat-hint>
</mat-form-field>
<mat-divider></mat-divider>
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12 mt-3">
<mat-checkbox color="accent" [(ngModel)]="new_config['Encryption']['use-encryption']"><ng-container i18n="Use encryption setting">Use encryption</ng-container></mat-checkbox>
</div>
<div class="col-12">
<mat-form-field color="accent">
<input [disabled]="!new_config['Encryption']['use-encryption']" [(ngModel)]="new_config['Encryption']['cert-file-path']" matInput placeholder="Cert file path" i18n-placeholder="Cert file path input placeholder">
</mat-form-field>
</div>
<div class="col-12">
<mat-form-field color="accent">
<input [disabled]="!new_config['Encryption']['use-encryption']" [(ngModel)]="new_config['Encryption']['key-file-path']" matInput placeholder="Key file path" i18n-placeholder="Key file path input placeholder">
</mat-form-field>
</div>
</div>
</div>
</div>
</div>
</mat-expansion-panel>
<mat-divider></mat-divider>
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12 mt-3">
<mat-checkbox color="accent" [(ngModel)]="new_config['Subscriptions']['allow_subscriptions']"><ng-container i18n="Allow subscriptions setting">Allow subscriptions</ng-container></mat-checkbox>
</div>
<div class="col-12">
<mat-form-field color="accent">
<input [disabled]="!new_config['Subscriptions']['allow_subscriptions']" [(ngModel)]="new_config['Subscriptions']['subscriptions_base_path']" matInput placeholder="Subscriptions base path" i18n-placeholder="Subscriptions base path input setting placeholder">
<mat-hint><ng-container i18n="Subscriptions base path setting input hint">Base path for videos from your subscribed channels and playlists. It is relative to YTDL-Material's root folder.</ng-container></mat-hint>
</mat-form-field>
</div>
<div class="col-12 mt-5">
<mat-form-field color="accent">
<input [disabled]="!new_config['Subscriptions']['allow_subscriptions']" [(ngModel)]="new_config['Subscriptions']['subscriptions_check_interval']" matInput placeholder="Check interval" i18n-placeholder="Check interval input setting placeholder">
<mat-hint><ng-container i18n="Check interval setting input hint">Unit is seconds, only include numbers.</ng-container></mat-hint>
</mat-form-field>
</div>
<div class="col-12 mt-4">
<mat-checkbox color="accent" [disabled]="!new_config['Subscriptions']['allow_subscriptions']" [(ngModel)]="new_config['Subscriptions']['subscriptions_use_youtubedl_archive']"><ng-container i18n="Use youtube-dl archive setting">Use youtube-dl archive</ng-container></mat-checkbox>
<p><a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#how-do-i-download-only-new-videos-from-a-playlist"><ng-container i18n="youtube-dl archive explanation prefix link">With youtube-dl's archive</ng-container></a>&nbsp;<ng-container i18n="youtube-dl archive explanation middle">feature, downloaded videos from your subscriptions get recorded in a text file in the subscriptions archive sub-directory.</ng-container></p>
<p><ng-container i18n="youtube-dl archive explanation suffix">This enables the ability to permanently delete videos from your subscriptions without unsubscribing, and allows you to record which videos you downloaded in case of data loss.</ng-container></p>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12 mt-3">
<mat-form-field>
<mat-label><ng-container i18n="Theme select label">Theme</ng-container></mat-label>
<mat-select color="accent" [(ngModel)]="new_config['Themes']['default_theme']">
<mat-option value="default"><ng-container i18n="Default theme label">Default</ng-container></mat-option>
<mat-option value="dark"><ng-container i18n="Dark theme label">Dark</ng-container></mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-12 mb-2">
<mat-checkbox color="accent" [(ngModel)]="new_config['Themes']['allow_theme_change']"><ng-container i18n="Allow theme change setting">Allow theme change</ng-container></mat-checkbox>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12 mt-3">
<mat-form-field>
<mat-label><ng-container i18n="Language select label">Language</ng-container></mat-label>
<mat-select (selectionChange)="localeSelectChanged($event.value)" [(value)]="initialLocale">
<mat-option *ngFor="let locale of supported_locales" [value]="locale">
{{all_locales[locale]['nativeName']}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
</ng-template>
</mat-tab>
<!-- Downloader -->
<mat-tab label="Downloader" i18n-label="Downloader settings label">
<ng-template matTabContent>
<!-- Downloader -->
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12 mt-3">
<mat-form-field color="accent">
<input matInput [(ngModel)]="new_config['Downloader']['path-audio']" placeholder="Audio folder path" i18n-placeholder="Audio folder path input placeholder" required>
<mat-hint><ng-container i18n="Aduio path setting input hint">Path for audio only downloads. It is relative to YTDL-Material's root folder.</ng-container></mat-hint>
</mat-form-field>
</div>
<div class="col-12 mt-5">
<mat-form-field color="accent">
<input matInput [(ngModel)]="new_config['Downloader']['path-video']" placeholder="Video folder path" i18n-placeholder="Video folder path input placeholder" required>
<mat-hint><ng-container i18n="Video path setting input hint">Path for video downloads. It is relative to YTDL-Material's root folder.</ng-container></mat-hint>
</mat-form-field>
</div>
<!-- Encryption -->
<mat-expansion-panel class="settings-expansion-panel mat-elevation-z4">
<mat-expansion-panel-header>
<mat-panel-title>
<ng-container i18n="Encryption settings title">Encryption</ng-container>
</mat-panel-title>
</mat-expansion-panel-header>
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12">
<mat-checkbox color="accent" [(ngModel)]="new_config['Encryption']['use-encryption']"><ng-container i18n="Use encryption setting">Use encryption</ng-container></mat-checkbox>
</div>
<div class="col-12">
<mat-form-field color="accent">
<input [disabled]="!new_config['Encryption']['use-encryption']" [(ngModel)]="new_config['Encryption']['cert-file-path']" matInput placeholder="Cert file path" i18n-placeholder="Cert file path input placeholder">
</mat-form-field>
</div>
<div class="col-12">
<mat-form-field color="accent">
<input [disabled]="!new_config['Encryption']['use-encryption']" [(ngModel)]="new_config['Encryption']['key-file-path']" matInput placeholder="Key file path" i18n-placeholder="Key file path input placeholder">
</mat-form-field>
</div>
</div>
</div>
</mat-expansion-panel>
<div class="col-12 mt-5">
<mat-form-field style="margin-right: 12px;" color="accent">
<textarea matInput [(ngModel)]="new_config['Downloader']['custom_args']" placeholder="Custom args" i18n-placeholder="Custom args input placeholder"></textarea>
<mat-hint><ng-container i18n="Custom args setting input hint">Global custom args for downloads on the home page.</ng-container></mat-hint>
<button class="args-edit-button" (click)="openArgsModifierDialog()" mat-icon-button><mat-icon>edit</mat-icon></button>
</mat-form-field>
</div>
<!-- Downloader -->
<mat-expansion-panel class="settings-expansion-panel mat-elevation-z4">
<mat-expansion-panel-header>
<mat-panel-title>
<ng-container i18n="Downloader settings title">Downloader</ng-container>
</mat-panel-title>
</mat-expansion-panel-header>
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12">
<mat-form-field color="accent">
<input matInput [(ngModel)]="new_config['Downloader']['path-audio']" placeholder="Audio folder path" i18n-placeholder="Audio folder path input placeholder" required>
<mat-hint><ng-container i18n="Aduio path setting input hint">Path for audio only downloads. It is relative to YTDL-Material's root folder.</ng-container></mat-hint>
</mat-form-field>
<div class="col-12 mt-4">
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['use_youtubedl_archive']"><ng-container i18n="Use youtubedl archive setting">Use youtube-dl archive</ng-container></mat-checkbox>
<p>Note: This setting only applies to downloads on the Home page. If you would like to use youtube-dl archive functionality in subscriptions, head down to the Subscriptions section.</p>
</div>
</div>
</div>
<div class="col-12 mt-4">
<mat-form-field color="accent">
<input matInput [(ngModel)]="new_config['Downloader']['path-video']" placeholder="Video folder path" i18n-placeholder="Video folder path input placeholder" required>
<mat-hint><ng-container i18n="Video path setting input hint">Path for video downloads. It is relative to YTDL-Material's root folder.</ng-container></mat-hint>
</mat-form-field>
</ng-template>
</mat-tab>
<!-- Extra -->
<mat-tab label="Extra" i18n-label="Extra settings label">
<ng-template matTabContent>
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12 mt-3">
<mat-form-field color="accent">
<input [(ngModel)]="new_config['Extra']['title_top']" matInput placeholder="Top title" i18n-placeholder="Top title input placeholder" required>
<mat-hint></mat-hint>
</mat-form-field>
</div>
<div class="col-12">
<mat-checkbox color="accent" [(ngModel)]="new_config['Extra']['file_manager_enabled']"><ng-container i18n="File manager enabled setting">File manager enabled</ng-container></mat-checkbox>
</div>
<div class="col-12">
<mat-checkbox color="accent" [(ngModel)]="new_config['Extra']['allow_quality_select']"><ng-container i18n="Allow quality seelct setting">Allow quality select</ng-container></mat-checkbox>
</div>
<div class="col-12">
<mat-checkbox color="accent" [(ngModel)]="new_config['Extra']['download_only_mode']"><ng-container i18n="Download only mode setting">Download only mode</ng-container></mat-checkbox>
</div>
<div class="col-12">
<mat-checkbox color="accent" [(ngModel)]="new_config['Extra']['allow_multi_download_mode']"><ng-container i18n="Allow multi-downloade mode setting">Allow multi-download mode</ng-container></mat-checkbox>
</div>
<div class="col-12">
<mat-checkbox color="accent" [(ngModel)]="new_config['Extra']['settings_pin_required']"><ng-container i18n="Require pin for settings setting">Require pin for settings</ng-container></mat-checkbox>
<button style="margin-left: 15px; margin-bottom: 10px;" mat-stroked-button (click)="setNewPin()" [disabled]="!new_config['Extra']['settings_pin_required']"><ng-container i18n="Set new pin button">Set New Pin</ng-container></button>
</div>
</div>
</div>
<div class="col-12 mt-4">
<mat-form-field color="accent">
<textarea matInput [(ngModel)]="new_config['Downloader']['custom_args']" placeholder="Custom args" i18n-placeholder="Custom args input placeholder"></textarea>
<mat-hint><ng-container i18n="Custom args setting input hint">Global custom args for downloads on the home page.</ng-container></mat-hint>
</mat-form-field>
<button style="margin-left: 12px;" (click)="openArgsModifierDialog()" mat-stroked-button>Modify args</button>
<mat-divider></mat-divider>
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12 mt-3">
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_API_key']"><ng-container i18n="Enable Public API key setting">Enable Public API</ng-container></mat-checkbox>
</div>
<div class="col-12 mb-3">
<div class="enable-api-key-div">
<mat-form-field color="accent">
<input [disabled]="!new_config['API']['use_API_key']" [(ngModel)]="new_config['API']['API_key']" matInput placeholder="Public API Key" i18n-placeholder="Public API Key setting placeholder" required>
<mat-hint><a target="_blank" href="https://stoplight.io/p/docs/gh/tzahi12345/youtubedl-material"><ng-container i18n="View API docs setting hint">View documentation</ng-container></a></mat-hint>
</mat-form-field>
</div>
<div class="api-key-div">
<button matTooltip-i18n matTooltip="This will delete your old API key!" mat-stroked-button (click)="generateAPIKey()"><ng-container i18n="Generate key button">Generate</ng-container></button>
</div>
</div>
</div>
</div>
<div class="col-12 mt-4">
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['use_youtubedl_archive']"><ng-container i18n="Use youtubedl archive setting">Use youtube-dl archive</ng-container></mat-checkbox>
<p>Note: This setting only applies to downloads on the Home page. If you would like to use youtube-dl archive functionality in subscriptions, head down to the Subscriptions section.</p>
<mat-divider></mat-divider>
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12 mt-3">
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_youtube_API']"><ng-container i18n="Use YouTube API setting">Use YouTube API</ng-container></mat-checkbox>
</div>
<div class="col-12 mb-3">
<mat-form-field color="accent">
<input [disabled]="!new_config['API']['use_youtube_API']" [(ngModel)]="new_config['API']['youtube_API_key']" matInput placeholder="Youtube API Key" i18n-placeholder="Youtube API Key setting placeholder" required>
<mat-hint><a target="_blank" href="https://developers.google.com/youtube/v3/getting-started"><ng-container i18n="Youtube API Key setting hint">Generating a key is easy!</ng-container></a></mat-hint>
</mat-form-field>
</div>
</div>
</div>
</div>
</div>
</mat-expansion-panel>
<!-- Extra -->
<mat-expansion-panel class="settings-expansion-panel mat-elevation-z4">
<mat-expansion-panel-header>
<mat-panel-title>
<ng-container i18n="Extra settings title">Extra</ng-container>
</mat-panel-title>
</mat-expansion-panel-header>
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12">
<mat-form-field color="accent">
<input [(ngModel)]="new_config['Extra']['title_top']" matInput placeholder="Top title" i18n-placeholder="Top title input placeholder" required>
<mat-hint></mat-hint>
</mat-form-field>
<mat-divider></mat-divider>
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12 mt-3">
<h6>Chrome</h6>
<p><a href="https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/chrome-extension/youtubedl-material-chrome-extension.zip?raw=true"><ng-container i18n="Chrome ext click here">Click here</ng-container></a>&nbsp;<ng-container i18n="Chrome click here suffix">to download the official YoutubeDL-Material Chrome extension manually.</ng-container></p>
<p><ng-container i18n="Chrome setup suffix">You must manually load the extension and modify the extension's settings to set the frontend URL.</ng-container></p>
<mat-divider class="ext-divider"></mat-divider>
</div>
<div class="col-12">
<h6>Firefox</h6>
<p><a href="https://addons.mozilla.org/en-US/firefox/addon/youtubedl-material/" target="_blank"><ng-container i18n="Firefox ext click here">Click here</ng-container></a>&nbsp;<ng-container i18n="Firefox click here suffix">to install the official YoutubeDL-Material Firefox extension right off the Firefox extensions page.</ng-container></p>
<p><a href="https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Firefox-Extension" target="_blank"><ng-container i18n="Firefox setup prefix link">Detailed setup instructions.</ng-container></a>&nbsp;<ng-container i18n="Firefox setup suffix">Not much is required other than changing the extension's settings to set the frontend URL.</ng-container></p>
<mat-divider class="ext-divider"></mat-divider>
</div>
<div class="col-12">
<h6>Bookmarklet</h6>
<p><ng-container i18n="Bookmarklet instructions">Drag the link below to your bookmarks, and you're good to go! Just navigate to the YouTube video you'd like to download, and click the bookmark.</ng-container></p>
<mat-checkbox (change)="bookmarkletAudioOnlyChanged($event)"><ng-container i18n="Generate audio only bookmarklet checkbox">Generate 'audio only' bookmarklet</ng-container></mat-checkbox>
<!--<button style="margin-bottom: 5px;" mat-stroked-button color="accent" (click)="generateBookmarklet()">Generate bookmarklet</button>-->
<p><a [href]="generated_bookmarklet_code" target="_blank">YTDL-Bookmarklet</a></p>
</div>
</div>
</div>
<div class="col-12">
<mat-checkbox color="accent" [(ngModel)]="new_config['Extra']['file_manager_enabled']"><ng-container i18n="File manager enabled setting">File manager enabled</ng-container></mat-checkbox>
</ng-template>
</mat-tab>
<!-- Advanced -->
<mat-tab label="Advanced" i18n-label="Host settings label">
<ng-template matTabContent>
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12 mt-3">
<mat-checkbox color="accent" [(ngModel)]="new_config['Advanced']['use_default_downloading_agent']"><ng-container i18n="Use default downloading agent setting">Use default downloading agent</ng-container></mat-checkbox>
</div>
<div class="col-12">
<mat-form-field>
<mat-label><ng-container i18n="Custom downloader select label">Select a downloader</ng-container></mat-label>
<mat-select [disabled]="new_config['Advanced']['use_default_downloading_agent']" color="accent" [(ngModel)]="new_config['Advanced']['custom_downloading_agent']">
<mat-option value="aria2c">aria2c</mat-option>
<mat-option value="avconv">avconv</mat-option>
<mat-option value="axel">axel</mat-option>
<mat-option value="curl">curl</mat-option>
<mat-option value="ffmpeg">ffmpeg</mat-option>
<mat-option value="httpie">httpie</mat-option>
<mat-option value="wget">wget</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="col-12 mb-3">
<mat-checkbox color="accent" [(ngModel)]="new_config['Advanced']['allow_advanced_download']"><ng-container i18n="Allow advanced downloading setting">Allow advanced download</ng-container></mat-checkbox>
</div>
</div>
</div>
<div class="col-12">
<mat-checkbox color="accent" [(ngModel)]="new_config['Extra']['allow_quality_select']"><ng-container i18n="Allow quality seelct setting">Allow quality select</ng-container></mat-checkbox>
<mat-divider></mat-divider>
<div *ngIf="new_config" class="container-fluid mt-1">
<app-updater></app-updater>
</div>
<div class="col-12">
<mat-checkbox color="accent" [(ngModel)]="new_config['Extra']['download_only_mode']"><ng-container i18n="Download only mode setting">Download only mode</ng-container></mat-checkbox>
</div>
<div class="col-12">
<mat-checkbox color="accent" [(ngModel)]="new_config['Extra']['allow_multi_download_mode']"><ng-container i18n="Allow multi-downloade mode setting">Allow multi-download mode</ng-container></mat-checkbox>
</div>
<div class="col-12">
<mat-checkbox color="accent" [(ngModel)]="new_config['Extra']['settings_pin_required']"><ng-container i18n="Require pin for settings setting">Require pin for settings</ng-container></mat-checkbox>
<button style="margin-left: 15px;" mat-stroked-button (click)="setNewPin()" [disabled]="!new_config['Extra']['settings_pin_required']"><ng-container i18n="Set new pin button">Set New Pin</ng-container></button>
</div>
</div>
</div>
</mat-expansion-panel>
<!-- API -->
<mat-expansion-panel class="settings-expansion-panel mat-elevation-z4">
<mat-expansion-panel-header>
<mat-panel-title>
<ng-container i18n="API settings title">API</ng-container>
</mat-panel-title>
</mat-expansion-panel-header>
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12">
<mat-checkbox color="accent" [(ngModel)]="new_config['API']['use_youtube_API']"><ng-container i18n="Use YouTube API setting">Use YouTube API</ng-container></mat-checkbox>
</div>
<div class="col-12">
<mat-form-field color="accent">
<input [disabled]="!new_config['API']['use_youtube_API']" [(ngModel)]="new_config['API']['youtube_API_key']" matInput placeholder="Youtube API Key" i18n-placeholder="Youtube API Key setting placeholder" required>
<mat-hint><a target="_blank" href="https://developers.google.com/youtube/v3/getting-started"><ng-container i18n="Youtube API Key setting hint">Generating a key is easy!</ng-container></a></mat-hint>
</mat-form-field>
</div>
</div>
</div>
</mat-expansion-panel>
<!-- Themes -->
<mat-expansion-panel class="settings-expansion-panel mat-elevation-z4">
<mat-expansion-panel-header>
<mat-panel-title>
<ng-container i18n="Themes settings title">Themes</ng-container>
</mat-panel-title>
</mat-expansion-panel-header>
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12">
<mat-select color="accent" style="width: 100px" [(ngModel)]="new_config['Themes']['default_theme']">
<mat-option value="default"><ng-container i18n="Default theme label">Default</ng-container></mat-option>
<mat-option value="dark"><ng-container i18n="Dark theme label">Dark</ng-container></mat-option>
</mat-select>
</div>
<div class="col-12 mt-4">
<mat-checkbox color="accent" [(ngModel)]="new_config['Themes']['allow_theme_change']"><ng-container i18n="Allow theme change setting">Allow theme change</ng-container></mat-checkbox>
</div>
</div>
</div>
</mat-expansion-panel>
<!-- Subscriptions -->
<mat-expansion-panel class="settings-expansion-panel mat-elevation-z4">
<mat-expansion-panel-header>
<mat-panel-title>
<ng-container i18n="Subscriptions settings title">Subscriptions</ng-container>
</mat-panel-title>
</mat-expansion-panel-header>
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12">
<mat-checkbox color="accent" [(ngModel)]="new_config['Subscriptions']['allow_subscriptions']"><ng-container i18n="Allow subscriptions setting">Allow subscriptions</ng-container></mat-checkbox>
</div>
<div class="col-12">
<mat-form-field color="accent">
<input [disabled]="!new_config['Subscriptions']['allow_subscriptions']" [(ngModel)]="new_config['Subscriptions']['subscriptions_base_path']" matInput placeholder="Subscriptions base path" i18n-placeholder="Subscriptions base path input setting placeholder">
<mat-hint><ng-container i18n="Subscriptions base path setting input hint">Base path for videos from your subscribed channels and playlists. It is relative to YTDL-Material's root folder.</ng-container></mat-hint>
</mat-form-field>
</div>
<div class="col-12 mt-5">
<mat-form-field color="accent">
<input [disabled]="!new_config['Subscriptions']['allow_subscriptions']" [(ngModel)]="new_config['Subscriptions']['subscriptions_check_interval']" matInput placeholder="Check interval" i18n-placeholder="Check interval input setting placeholder">
<mat-hint><ng-container i18n="Check interval setting input hint">Unit is seconds, only include numbers.</ng-container></mat-hint>
</mat-form-field>
</div>
<div class="col-12 mt-4">
<mat-checkbox color="accent" [disabled]="!new_config['Subscriptions']['allow_subscriptions']" [(ngModel)]="new_config['Subscriptions']['subscriptions_use_youtubedl_archive']"><ng-container i18n="Use youtube-dl archive setting">Use youtube-dl archive</ng-container></mat-checkbox>
<p><a target="_blank" href="https://github.com/ytdl-org/youtube-dl/blob/master/README.md#how-do-i-download-only-new-videos-from-a-playlist"><ng-container i18n="youtube-dl archive explanation prefix link">With youtube-dl's archive</ng-container></a>&nbsp;<ng-container i18n="youtube-dl archive explanation middle">feature, downloaded videos from your subscriptions get recorded in a text file in the subscriptions archive sub-directory.</ng-container></p>
<p><ng-container i18n="youtube-dl archive explanation suffix">This enables the ability to permanently delete videos from your subscriptions without unsubscribing, and allows you to record which videos you downloaded in case of data loss.</ng-container></p>
</div>
</div>
</div>
</mat-expansion-panel>
<!-- Extensions -->
<mat-expansion-panel class="settings-expansion-panel mat-elevation-z4">
<mat-expansion-panel-header>
<mat-panel-title>
<ng-container i18n="Extensions settings title">Extensions</ng-container>
</mat-panel-title>
</mat-expansion-panel-header>
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12">
<h6>Chrome</h6>
<p><a href="https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/chrome-extension/youtubedl-material-chrome-extension.zip?raw=true"><ng-container i18n="Chrome ext click here">Click here</ng-container></a>&nbsp;<ng-container i18n="Chrome click here suffix">to download the official YoutubeDL-Material Chrome extension manually.</ng-container></p>
<p><ng-container i18n="Chrome setup suffix">You must manually load the extension and modify the extension's settings to set the frontend URL.</ng-container></p>
<mat-divider class="ext-divider"></mat-divider>
</div>
<div class="col-12">
<h6>Firefox</h6>
<p><a href="https://addons.mozilla.org/en-US/firefox/addon/youtubedl-material/" target="_blank"><ng-container i18n="Firefox ext click here">Click here</ng-container></a>&nbsp;<ng-container i18n="Firefox click here suffix">to install the official YoutubeDL-Material Firefox extension right off the Firefox extensions page.</ng-container></p>
<p><a href="https://github.com/Tzahi12345/YoutubeDL-Material/wiki/Firefox-Extension" target="_blank"><ng-container i18n="Firefox setup prefix link">Detailed setup instructions.</ng-container></a>&nbsp;<ng-container i18n="Firefox setup suffix">Not much is required other than changing the extension's settings to set the frontend URL.</ng-container></p>
<mat-divider class="ext-divider"></mat-divider>
</div>
<div class="col-12">
<h6>Bookmarklet</h6>
<p><ng-container i18n="Bookmarklet instructions">Drag the link below to your bookmarks, and you're good to go! Just navigate to the YouTube video you'd like to download, and click the bookmark.</ng-container></p>
<!--<button style="margin-bottom: 5px;" mat-stroked-button color="accent" (click)="generateBookmarklet()">Generate bookmarklet</button>-->
<p><a [href]="generated_bookmarklet_code" target="_blank">YTDL-Bookmarklet</a></p>
</div>
</div>
</div>
</mat-expansion-panel>
<!-- Advanced -->
<mat-expansion-panel class="settings-expansion-panel mat-elevation-z4">
<mat-expansion-panel-header>
<mat-panel-title>
<ng-container i18n="Advanced settings title">Advanced</ng-container>
</mat-panel-title>
</mat-expansion-panel-header>
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12">
<mat-checkbox color="accent" [(ngModel)]="new_config['Advanced']['use_default_downloading_agent']"><ng-container i18n="Use default downloading agent setting">Use default downloading agent</ng-container></mat-checkbox>
</div>
<div class="col-12 my-2">
<mat-select [disabled]="new_config['Advanced']['use_default_downloading_agent']" color="accent" style="width: 200px" [(ngModel)]="new_config['Advanced']['custom_downloading_agent']">
<mat-option value="">Select a downloader</mat-option>
<mat-option value="aria2c">aria2c</mat-option>
<mat-option value="avconv">avconv</mat-option>
<mat-option value="axel">axel</mat-option>
<mat-option value="curl">curl</mat-option>
<mat-option value="ffmpeg">ffmpeg</mat-option>
<mat-option value="httpie">httpie</mat-option>
<mat-option value="wget">wget</mat-option>
</mat-select>
</div>
<div class="col-12 mt-2">
<mat-checkbox color="accent" [(ngModel)]="new_config['Advanced']['allow_advanced_download']"><ng-container i18n="Allow advanced downloading setting">Allow advanced download</ng-container></mat-checkbox>
</div>
</div>
</div>
</mat-expansion-panel>
</ng-template>
</mat-tab>
</mat-tab-group>
</mat-dialog-content>
<mat-dialog-actions>

View File

@@ -2,11 +2,22 @@
margin-bottom: 20px;
}
.locale-select {
margin-bottom: 10px;
width: 130px;
}
.ext-divider {
margin-bottom: 14px;
}
.args-edit-button {
position: absolute;
margin-left: 10px;
top: 20px;
}
.enable-api-key-div {
display: inline-block;
margin-bottom: 8px;
margin-right: 15px;
}
.api-key-div {
display: inline-block;
}

View File

@@ -6,6 +6,8 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import {DomSanitizer} from '@angular/platform-browser';
import { MatDialog } from '@angular/material/dialog';
import { ArgModifierDialogComponent } from 'app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component';
import { CURRENT_VERSION } from 'app/consts';
import { MatCheckboxChange } from '@angular/material/checkbox';
@Component({
selector: 'app-settings',
@@ -21,9 +23,13 @@ export class SettingsComponent implements OnInit {
new_config = null
loading_config = false;
generated_bookmarklet_code = null;
bookmarkletAudioOnly = false;
_settingsSame = true;
latestGithubRelease = null;
CURRENT_VERSION = CURRENT_VERSION
get settingsAreTheSame() {
this._settingsSame = this.settingsSame()
return this._settingsSame;
@@ -40,6 +46,8 @@ export class SettingsComponent implements OnInit {
this.getConfig();
this.generated_bookmarklet_code = this.sanitizer.bypassSecurityTrustUrl(this.generateBookmarkletCode());
this.getLatestGithubRelease();
}
getConfig() {
@@ -78,6 +86,15 @@ export class SettingsComponent implements OnInit {
});
}
generateAPIKey() {
this.postsService.generateNewAPIKey().subscribe(res => {
if (res['new_api_key']) {
this.initial_config.API.API_key = res['new_api_key'];
this.new_config.API.API_key = res['new_api_key'];
}
});
}
localeSelectChanged(new_val) {
localStorage.setItem('locale', new_val);
this.openSnackBar('Language successfully changed! Reload to update the page.')
@@ -90,11 +107,17 @@ export class SettingsComponent implements OnInit {
generateBookmarkletCode() {
const currentURL = window.location.href.split('#')[0];
const homePageWithArgsURL = currentURL + '#/home;url=';
const bookmarkletCodeInside = `'${homePageWithArgsURL}' + window.location`
const bookmarkletCode = `javascript:(function()%7Bwindow.open('${homePageWithArgsURL}' + encodeURIComponent(window.location))%7D)()`;
const audioOnly = this.bookmarkletAudioOnly;
// tslint:disable-next-line: max-line-length
const bookmarkletCode = `javascript:(function()%7Bwindow.open('${homePageWithArgsURL}' + encodeURIComponent(window.location) + ';audioOnly=${audioOnly}')%7D)()`;
return bookmarkletCode;
}
bookmarkletAudioOnlyChanged(event: MatCheckboxChange): void {
this.bookmarkletAudioOnly = event.checked;
this.generated_bookmarklet_code = this.sanitizer.bypassSecurityTrustUrl(this.generateBookmarkletCode());
}
// not currently functioning on most platforms. hence not in use
bookmarksite(title, url) {
// Internet Explorer
@@ -129,6 +152,12 @@ export class SettingsComponent implements OnInit {
});
}
getLatestGithubRelease() {
this.postsService.getLatestGithubRelease().subscribe(res => {
this.latestGithubRelease = res;
});
}
// snackbar helper
public openSnackBar(message: string, action: string = '') {
this.snackBar.open(message, action, {

View File

@@ -54,7 +54,11 @@ export class SubscriptionFileCardComponent implements OnInit {
}
goToFile() {
this.goToFileEmit.emit(this.file.id);
const emit_obj = {
name: this.file.id,
url: this.file.requested_formats ? this.file.requested_formats[0].url : this.file.url
}
this.goToFileEmit.emit(emit_obj);
}
openSubscriptionInfoDialog() {

View File

@@ -84,10 +84,16 @@ export class SubscriptionComponent implements OnInit {
});
}
goToFile(name) {
goToFile(emit_obj) {
const name = emit_obj['name'];
const url = emit_obj['url'];
localStorage.setItem('player_navigator', this.router.url);
this.router.navigate(['/player', {fileNames: name, type: 'subscription', subscriptionName: this.subscription.name,
subPlaylist: this.subscription.isPlaylist}]);
if (this.subscription.streamingOnly) {
this.router.navigate(['/player', {name: name, url: url}]);
} else {
this.router.navigate(['/player', {fileNames: name, type: 'subscription', subscriptionName: this.subscription.name,
subPlaylist: this.subscription.isPlaylist}]);
}
}
onSearchInputChanged(newvalue) {

View File

@@ -0,0 +1,18 @@
<div style="display: block">
<div style="display: inline-block">
<ng-container i18n="Select a version">Select a version:</ng-container>
</div>
<div *ngIf="availableVersions" style="display: inline-block; margin-left: 15px;">
<mat-form-field>
<mat-select [(ngModel)]="selectedVersion">
<mat-option *ngFor="let version of availableVersionsFiltered" [value]="version['tag_name']">
{{version['tag_name'] + (version === latestStableRelease ? ' - Latest Stable' : '') + (version['tag_name'] === CURRENT_VERSION ? ' - Current Version' : '')}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="selectedVersion && selectedVersion !== CURRENT_VERSION" style="display: inline-block; margin-left: 15px;">
<button (click)="updateServer()" color="accent" mat-raised-button><mat-icon>update</mat-icon>&nbsp;&nbsp;
<ng-container *ngIf="selectedVersion > CURRENT_VERSION">Upgrade to</ng-container><ng-container *ngIf="selectedVersion < CURRENT_VERSION">Downgrade to</ng-container>&nbsp;{{selectedVersion}}</button>
</div>
</div>

View File

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { UpdaterComponent } from './updater.component';
describe('UpdaterComponent', () => {
let component: UpdaterComponent;
let fixture: ComponentFixture<UpdaterComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ UpdaterComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(UpdaterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,62 @@
import { Component, OnInit } from '@angular/core';
import { PostsService } from 'app/posts.services';
import { CURRENT_VERSION } from 'app/consts';
import { MatDialog } from '@angular/material/dialog';
import { UpdateProgressDialogComponent } from 'app/dialogs/update-progress-dialog/update-progress-dialog.component';
@Component({
selector: 'app-updater',
templateUrl: './updater.component.html',
styleUrls: ['./updater.component.scss']
})
export class UpdaterComponent implements OnInit {
availableVersions = null;
availableVersionsFiltered = [];
versionsShowLimit = 5;
latestStableRelease = null;
selectedVersion = null;
CURRENT_VERSION = CURRENT_VERSION;
constructor(private postsService: PostsService, private dialog: MatDialog) { }
ngOnInit(): void {
this.getAvailableVersions();
}
updateServer() {
this.postsService.updateServer(this.selectedVersion).subscribe(res => {
if (res['success']) {
this.openUpdateProgressDialog();
}
});
}
getAvailableVersions() {
this.availableVersionsFiltered = [];
this.postsService.getAvailableRelease().subscribe(res => {
this.availableVersions = res;
for (let i = 0; i < this.availableVersions.length; i++) {
const currentVersion = this.availableVersions[i];
// if a stable release has not been found and the version is not "rc" (meaning it's stable) then set it as the stable release
if (!this.latestStableRelease && !currentVersion.tag_name.includes('rc')) {
this.latestStableRelease = currentVersion;
this.selectedVersion = this.latestStableRelease.tag_name;
}
if (this.latestStableRelease && i >= this.versionsShowLimit) {
break;
}
this.availableVersionsFiltered.push(currentVersion);
}
});
}
openUpdateProgressDialog() {
this.dialog.open(UpdateProgressDialogComponent, {
minWidth: '300px',
minHeight: '200px'
});
}
}

View File

@@ -16,7 +16,7 @@
"custom_args": ""
},
"Extra": {
"title_top": "Youtube Downloader",
"title_top": "YoutubeDL-Material",
"file_manager_enabled": true,
"allow_quality_select": true,
"download_only_mode": false,
@@ -24,6 +24,8 @@
"settings_pin_required": false
},
"API": {
"use_API_key": false,
"API_key": "",
"use_youtube_API": false,
"youtube_API_key": ""
},

View File

@@ -100,10 +100,6 @@
<context context-type="sourcefile">app/dialogs/arg-modifier-dialog/arg-modifier-dialog.component.html</context>
<context context-type="linenumber">66</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">289</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/subscribe-dialog/subscribe-dialog.component.html</context>
<context context-type="linenumber">37</context>
@@ -514,7 +510,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">22</context>
<context context-type="linenumber">24</context>
</context-group>
<note priority="1" from="description">Settings title</note>
</trans-unit>
@@ -774,7 +770,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">17</context>
<context context-type="linenumber">19</context>
</context-group>
<note priority="1" from="description">Dark theme label</note>
</trans-unit>
@@ -794,7 +790,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">38</context>
<context context-type="linenumber">40</context>
</context-group>
<note priority="1" from="description">Subscriptions settings title</note>
</trans-unit>
@@ -882,7 +878,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/about-dialog/about-dialog.component.html</context>
<context context-type="linenumber">12</context>
<context context-type="linenumber">25</context>
</context-group>
<note priority="1" from="description">Chrome ext click here</note>
</trans-unit>
@@ -966,6 +962,14 @@
</context-group>
<note priority="1" from="description">Settings save button</note>
</trans-unit>
<trans-unit id="fe8fd36dbf5deee1d56564965787a782a66eba44" datatype="html">
<source>{VAR_SELECT, select, true {Close} false {Cancel} other {otha} }</source>
<context-group purpose="location">
<context context-type="sourcefile">app/settings/settings.component.html</context>
<context context-type="linenumber">289</context>
</context-group>
<note priority="1" from="description">Settings cancel and close button</note>
</trans-unit>
<trans-unit id="cec82c0a545f37420d55a9b6c45c20546e82f94e" datatype="html">
<source>About YoutubeDL-Material</source>
<context-group purpose="location">
@@ -978,23 +982,47 @@
<source>is an open-source YouTube downloader built under Google&apos;s Material Design specifications. You can seamlessly download your favorite videos as video or audio files, and even subscribe to your favorite channels and playlists to keep updated with their new videos.</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/about-dialog/about-dialog.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">12</context>
</context-group>
<note priority="1" from="description">About first paragraph</note>
</trans-unit>
<trans-unit id="c072eebcb5b1f1eef6fb2ee1756e839dd302f3de" datatype="html">
<source>has some awesome features included! An extensive API, Docker support, and localization (translation) support. Read up on all the supported features by clicking on the GitHub icon below.</source>
<trans-unit id="bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7" datatype="html">
<source>has some awesome features included! An extensive API, Docker support, and localization (translation) support. Read up on all the supported features by clicking on the GitHub icon above.</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/about-dialog/about-dialog.component.html</context>
<context context-type="linenumber">9</context>
<context context-type="linenumber">15</context>
</context-group>
<note priority="1" from="description">About second paragraph</note>
</trans-unit>
<trans-unit id="a45e3b05f0529dc5246d70ef62304c94426d4c81" datatype="html">
<source>Installed version:</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/about-dialog/about-dialog.component.html</context>
<context context-type="linenumber">20</context>
</context-group>
<note priority="1" from="description">Version label</note>
</trans-unit>
<trans-unit id="e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe" datatype="html">
<source>Checking for updates...</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/about-dialog/about-dialog.component.html</context>
<context context-type="linenumber">20</context>
</context-group>
<note priority="1" from="description">Checking for updates text</note>
</trans-unit>
<trans-unit id="a16e92385b4fd9677bb830a4b796b8b79c113290" datatype="html">
<source>Update available</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/about-dialog/about-dialog.component.html</context>
<context context-type="linenumber">21</context>
</context-group>
<note priority="1" from="description">View latest update</note>
</trans-unit>
<trans-unit id="b33536f59b94ec935a16bd6869d836895dc5300c" datatype="html">
<source>Found a bug or have a suggestion?</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/about-dialog/about-dialog.component.html</context>
<context context-type="linenumber">12</context>
<context context-type="linenumber">25</context>
</context-group>
<note priority="1" from="description">About bug prefix</note>
</trans-unit>
@@ -1002,31 +1030,15 @@
<source>to create an issue!</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/about-dialog/about-dialog.component.html</context>
<context context-type="linenumber">12</context>
<context context-type="linenumber">25</context>
</context-group>
<note priority="1" from="description">About bug suffix</note>
</trans-unit>
<trans-unit id="a45e3b05f0529dc5246d70ef62304c94426d4c81" datatype="html">
<source>Installed version:</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/about-dialog/about-dialog.component.html</context>
<context context-type="linenumber">17</context>
</context-group>
<note priority="1" from="description">Version label</note>
</trans-unit>
<trans-unit id="effdc7dfbbc49c08d25ea1748fca00c38c918abd" datatype="html">
<source>View latest update</source>
<context-group purpose="location">
<context context-type="sourcefile">app/dialogs/about-dialog/about-dialog.component.html</context>
<context context-type="linenumber">17</context>
</context-group>
<note priority="1" from="description">View latest update</note>
</trans-unit>
<trans-unit id="004b222ff9ef9dd4771b777950ca1d0e4cd4348a" datatype="html">
<source>About</source>
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">26</context>
<context context-type="linenumber">28</context>
</context-group>
<note priority="1" from="description">About menu label</note>
</trans-unit>
@@ -1034,7 +1046,7 @@
<source>Home</source>
<context-group purpose="location">
<context context-type="sourcefile">app/app.component.html</context>
<context context-type="linenumber">37</context>
<context context-type="linenumber">39</context>
</context-group>
<note priority="1" from="description">Navigation menu Home Page title</note>
</trans-unit>

View File

@@ -106,13 +106,15 @@
"5fab47f146b0a4b809dcebf3db9da94df6299ea1": "Usar agente de descarga predeterminado",
"dc3d990391c944d1fbfc7cfb402f7b5e112fb3a8": "Permitir descarga avanzada",
"52c9a103b812f258bcddc3d90a6e3f46871d25fe": "Salvar",
"fe8fd36dbf5deee1d56564965787a782a66eba44": "{VAR_SELECT, select, true {Cerrar} false {Cancelar} other {Otro} }",
"cec82c0a545f37420d55a9b6c45c20546e82f94e": "Sobre YoutubeDL-Material",
"199c17e5d6a419313af3c325f06dcbb9645ca618": "es un descargador de código abierto de YouTube creado bajo las especificaciones de \"Material Design\" de Google. Puede descargar sin problemas sus videos favoritos como archivos de video o audio, e incluso suscribirse a sus canales favoritos y listas de reproducción para mantenerse actualizado con sus nuevos videos.",
"c072eebcb5b1f1eef6fb2ee1756e839dd302f3de": "tiene algunas características increíbles incluidas! Una amplia API, soporte de Docker y soporte de localización (traducción). Lea todas las funciones admitidas haciendo clic en el icono de GitHub a continuación.",
"bc0ad0ee6630acb7fcb7802ec79f5a0ee943c1a7": "tiene algunas características increíbles incluidas! Una amplia API, soporte de Docker y soporte de localización (traducción). Lea todas las funciones compatibles haciendo clic en el icono de GitHub que se encuentra arriba.",
"a45e3b05f0529dc5246d70ef62304c94426d4c81": "Versión instalada:",
"e22f3a5351944f3a1a10cfc7da6f65dfbe0037fe": "Comprobando actualizaciones...",
"a16e92385b4fd9677bb830a4b796b8b79c113290": "Actualización disponible",
"b33536f59b94ec935a16bd6869d836895dc5300c": "¿Encontró un error o tiene una sugerencia?",
"e1f398f38ff1534303d4bb80bd6cece245f24016": "para crear una cuestión!",
"a45e3b05f0529dc5246d70ef62304c94426d4c81": "Versión instalada:",
"effdc7dfbbc49c08d25ea1748fca00c38c918abd": "Ver la última actualización",
"004b222ff9ef9dd4771b777950ca1d0e4cd4348a": "Sobre",
"92eee6be6de0b11c924e3ab27db30257159c0a7c": "Inicio",
"5b3075e8dc3f3921ec316b0bd83b6d14a06c1a4f": "Guardar cambios",

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,6 @@ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { environment } from './environments/environment';
import { loadTranslations } from '@angular/localize';
import { getTranslations, ParsedTranslationBundle } from '@locl/core';
if (environment.production) {
enableProdMode();
@@ -17,8 +16,7 @@ if (!locale) {
localStorage.setItem('locale', 'en');
}
if (locale && locale !== 'en') {
getTranslations(`./assets/i18n/messages.${locale}.json`).then(
(data: ParsedTranslationBundle) => {
fetch(`./assets/i18n/messages.${locale}.json`).then(res => res.json()).then((data) => {
loadTranslations(data as any);
import('./app/app.module').then(module => {
platformBrowserDynamic()

View File

@@ -30,7 +30,7 @@ $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-light-blue);
$dark-primary: mat-palette($mat-light-blue, 700, 100, 800);
$dark-accent: mat-palette($mat-blue);
$dark-warn: mat-palette($mat-deep-orange);