Compare commits

...

32 Commits

Author SHA1 Message Date
Isaac Abadi
004a234b02 Downloads are now properly assigned a filename 2020-09-21 00:27:49 -04:00
Isaac Abadi
daca715d1b Fixes bug where playlists could not have download progress tracked
- downloads are now treated as playlists for cleaner logic
2020-09-20 23:01:43 -04:00
Isaac Abadi
8fdc231f08 Updated new home page UI to support file manager disabling and permissions
- file manager enabled state is now cached for faster loading
2020-09-18 11:22:45 -04:00
Isaac Abadi
ae8f7a2a33 Fixed bug that prevented playlists from being navigated to 2020-09-18 11:05:13 -04:00
Tzahi12345
d0782bb444 Update README.md
Updated API docs

Fixes #213
2020-09-18 00:46:42 -04:00
Isaac Abadi
49210abb49 Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material 2020-09-17 03:15:29 -04:00
Isaac Abadi
851bfb81ba File cards are now properly centered 2020-09-17 03:12:09 -04:00
Isaac Abadi
35d0d439fa Control-clicking file cards will now open the player in a new tab 2020-09-17 03:11:52 -04:00
Tzahi12345
ded3ad6dfc Merge pull request #212 from Tzahi12345/dependabot/npm_and_yarn/backend/node-fetch-2.6.1
Bump node-fetch from 2.6.0 to 2.6.1 in /backend
2020-09-12 17:07:53 -04:00
dependabot[bot]
61daf26641 Bump node-fetch from 2.6.0 to 2.6.1 in /backend
Bumps [node-fetch](https://github.com/bitinn/node-fetch) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/bitinn/node-fetch/releases)
- [Changelog](https://github.com/node-fetch/node-fetch/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/bitinn/node-fetch/compare/v2.6.0...v2.6.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-12 20:55:44 +00:00
Tzahi12345
95e53b9549 Fixed bug where unix paths would improperly parsed while importing unregistered files 2020-09-07 16:06:25 -04:00
Tzahi12345
46ed0fe992 Fixed bug in import unregistered logic where files in subfolders could not be found 2020-09-07 00:39:27 -04:00
Isaac Abadi
082252ab1e Updated sidenav logic for "side" mode, where it will now autoclose in the player, be open everywhere else 2020-08-31 15:21:58 -04:00
Tzahi12345
5eccaa13e5 Merge pull request #206 from Tzahi12345/downloader-improvements
Downloader improvements - updated system and bug fixes
2020-08-31 15:17:40 -04:00
Isaac Abadi
71633950b2 Comments cleanup 2020-08-31 15:03:04 -04:00
Isaac Abadi
f31dad0215 JSON metadata files are no longer kept if the associated setting is not enabled 2020-08-30 05:56:25 -04:00
Isaac Abadi
7efbe40bb2 Added setting for including metadata/thumbnails in the UI 2020-08-30 05:55:50 -04:00
Isaac Abadi
5b768b5bda JSON blobs were accidentally inserted into DB, stringifying then parsing the video file object fixes this 2020-08-30 05:42:52 -04:00
Isaac Abadi
365cbc3ffa Mkv/webm formats are now included for quality select (will get merged into mp4 at the end) 2020-08-29 23:08:23 -04:00
Isaac Abadi
44647f3306 Download progress is now shown when downloads are 1% complete or more (it was 15% before) 2020-08-29 23:06:40 -04:00
Isaac Abadi
8a7409478a Added the ability to download videos at higher resolutions than the highest mp4 (fixes #76)
Deprecates normal downloading method. The "safe" method is now always used, and download progress is now estimated using the predicted end file size

Thumbnails are now auto downloaded along with the other metadata
2020-08-29 23:05:37 -04:00
Tzahi12345
70159813e5 Merge pull request #205 from Tzahi12345/add-ldap-auth
Added ability to register/login through LDAP
2020-08-26 04:30:43 -04:00
Tzahi12345
d292275956 Unfinished subscriptions will no longer cause an error during server startup 2020-08-24 05:13:27 -04:00
Tzahi12345
ba2acedb94 Files are now reloaded when you navigate back home 2020-08-24 05:13:01 -04:00
Tzahi12345
aa0558b770 Subscriptions are now reloaded on subscribe/unsubscribe in PostsService 2020-08-24 05:11:56 -04:00
Tzahi12345
d7f04fc90a Text for file duration in the unified file card component is now always black 2020-08-24 05:11:04 -04:00
Tzahi12345
087c9f1bb1 Added public directory to the gitignore 2020-08-24 02:44:52 -04:00
Tzahi12345
f874617965 Fixes bug where cached JWT token could prevent default admin creation 2020-08-24 02:44:39 -04:00
Tzahi12345
8fb8543829 Merge pull request #203 from Tzahi12345/arm-autobuild-test
Fix ARM autobuild
2020-08-24 02:20:19 -04:00
Tzahi12345
70d89d310c Removed unneeded hooks 2020-08-24 02:18:39 -04:00
Tzahi12345
c48aaaf13c Possible fix for arm autobuild (2) 2020-08-24 00:25:59 -04:00
Tzahi12345
6cf7ea193a Possible fix for arm autobuild 2020-08-24 00:21:10 -04:00
28 changed files with 370 additions and 160 deletions

1
.gitignore vendored
View File

@@ -65,3 +65,4 @@ backend/appdata/logs/error.log
backend/appdata/users.json backend/appdata/users.json
backend/users/* backend/users/*
backend/appdata/cookies.txt backend/appdata/cookies.txt
backend/public

View File

@@ -90,7 +90,7 @@ environment:
## API ## API
[API Docs](https://stoplight.io/p/docs/gh/tzahi12345/youtubedl-material?group=master&utm_campaign=publish_dialog&utm_source=studio) [API Docs](https://youtubedl-material.stoplight.io/docs/youtubedl-material/Public%20API%20v1.yaml)
To get started, go to the settings menu and enable the public API from the *Extra* tab. You can generate an API key if one is missing. To get started, go to the settings menu and enable the public API from the *Extra* tab. You can generate an API key if one is missing.

View File

@@ -1,11 +1,15 @@
FROM arm32v7/alpine:3.12 as frontend FROM alpine:3.12 as frontend
RUN apk add --no-cache \ RUN apk add --no-cache \
npm npm \
curl
RUN npm install -g @angular/cli RUN npm install -g @angular/cli
WORKDIR /build WORKDIR /build
RUN curl -L https://github.com/balena-io/qemu/releases/download/v3.0.0%2Bresin/qemu-3.0.0+resin-arm.tar.gz | tar zxvf - -C . && mv qemu-3.0.0+resin-arm/qemu-arm-static .
COPY [ "package.json", "package-lock.json", "/build/" ] COPY [ "package.json", "package-lock.json", "/build/" ]
RUN npm install RUN npm install
@@ -17,7 +21,7 @@ RUN ng build --prod
FROM arm32v7/alpine:3.12 FROM arm32v7/alpine:3.12
COPY qemu-arm-static /usr/bin COPY --from=frontend /build/qemu-arm-static /usr/bin
ENV UID=1000 \ ENV UID=1000 \
GID=1000 \ GID=1000 \

View File

@@ -7,7 +7,7 @@ var path = require('path');
var youtubedl = require('youtube-dl'); var youtubedl = require('youtube-dl');
var ffmpeg = require('fluent-ffmpeg'); var ffmpeg = require('fluent-ffmpeg');
var compression = require('compression'); var compression = require('compression');
var https = require('https'); var glob = require("glob")
var multer = require('multer'); var multer = require('multer');
var express = require("express"); var express = require("express");
var bodyParser = require("body-parser"); var bodyParser = require("body-parser");
@@ -1146,12 +1146,34 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
type: type, type: type,
percent_complete: 0, percent_complete: 0,
is_playlist: url.includes('playlist'), is_playlist: url.includes('playlist'),
timestamp_start: Date.now() timestamp_start: Date.now(),
filesize: null
}; };
const download = downloads[session][download_uid]; const download = downloads[session][download_uid];
updateDownloads(); updateDownloads();
// get video info prior to download
const info = await getVideoInfoByURL(url, downloadConfig, download);
if (!info) {
resolve(false);
return;
} else {
// store info in download for future use
if (Array.isArray(info)) {
download['fileNames'] = [];
for (let info_obj of info) download['fileNames'].push(info_obj['_filename']);
} else {
download['_filename'] = info['_filename'];
}
download['filesize'] = utils.getExpectedFileSize(info);
}
const download_checker = setInterval(() => checkDownloadPercent(download), 1000);
// download file
youtubedl.exec(url, downloadConfig, {}, function(err, output) { youtubedl.exec(url, downloadConfig, {}, function(err, output) {
clearInterval(download_checker); // stops the download checker from running as the download finished (or errored)
download['downloading'] = false; download['downloading'] = false;
download['timestamp_end'] = Date.now(); download['timestamp_end'] = Date.now();
var file_uid = null; var file_uid = null;
@@ -1164,7 +1186,7 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
download['error'] = err.stderr; download['error'] = err.stderr;
updateDownloads(); updateDownloads();
resolve(false); resolve(false);
throw err; return;
} else if (output) { } else if (output) {
if (output.length === 0 || output[0].length === 0) { if (output.length === 0 || output[0].length === 0) {
download['error'] = 'No output. Check if video already exists in your archive.'; download['error'] = 'No output. Check if video already exists in your archive.';
@@ -1407,7 +1429,7 @@ async function generateArgs(url, type, options) {
var youtubePassword = options.youtubePassword; var youtubePassword = options.youtubePassword;
let downloadConfig = null; let downloadConfig = null;
let qualityPath = (is_audio && !options.skip_audio_args) ? ['-f', 'bestaudio'] : ['-f', 'best[ext=mp4]']; let qualityPath = (is_audio && !options.skip_audio_args) ? ['-f', 'bestaudio'] : ['-f', 'bestvideo+bestaudio', '--merge-output-format', 'mp4'];
const is_youtube = url.includes('youtu'); const is_youtube = url.includes('youtu');
if (!is_audio && !is_youtube) { if (!is_audio && !is_youtube) {
// tiktok videos fail when using the default format // tiktok videos fail when using the default format
@@ -1485,6 +1507,10 @@ async function generateArgs(url, type, options) {
downloadConfig.push('--download-archive', merged_path); downloadConfig.push('--download-archive', merged_path);
} }
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
downloadConfig.push('--write-thumbnail');
}
if (globalArgs && globalArgs !== '') { if (globalArgs && globalArgs !== '') {
// adds global args // adds global args
if (downloadConfig.indexOf('-o') !== -1 && globalArgs.split(',,').indexOf('-o') !== -1) { if (downloadConfig.indexOf('-o') !== -1 && globalArgs.split(',,').indexOf('-o') !== -1) {
@@ -1497,11 +1523,36 @@ async function generateArgs(url, type, options) {
} }
logger.verbose(`youtube-dl args being used: ${downloadConfig.join(',')}`); logger.verbose(`youtube-dl args being used: ${downloadConfig.join(',')}`);
// downloadConfig.map((arg) => `"${arg}"`);
resolve(downloadConfig); resolve(downloadConfig);
}); });
} }
async function getVideoInfoByURL(url, args = [], download = null) {
return new Promise(resolve => {
// remove bad args
const new_args = [...args];
const archiveArgIndex = new_args.indexOf('--download-archive');
if (archiveArgIndex !== -1) {
new_args.splice(archiveArgIndex, 2);
}
// actually get info
youtubedl.getInfo(url, new_args, (err, output) => {
if (output) {
resolve(output);
} else {
logger.error(`Error while retrieving info on video with URL ${url} with the following message: ${err}`);
if (download) {
download['error'] = `Failed pre-check for video info: ${err}`;
updateDownloads();
}
resolve(null);
}
});
});
}
// currently only works for single urls // currently only works for single urls
async function getUrlInfos(urls) { async function getUrlInfos(urls) {
let startDate = Date.now(); let startDate = Date.now();
@@ -1559,47 +1610,36 @@ function updateDownloads() {
db.assign({downloads: downloads}).write(); db.assign({downloads: downloads}).write();
} }
/* function checkDownloadPercent(download) {
function checkDownloads() { /*
for (let [session_id, session_downloads] of Object.entries(downloads)) { This is more of an art than a science, we're just selecting files that start with the file name,
for (let [download_uid, download_obj] of Object.entries(session_downloads)) { thus capturing the parts being downloaded in files named like so: '<video title>.<format>.<ext>.part'.
if (download_obj && !download_obj['complete'] && !download_obj['error']
&& download_obj.timestamp_start > timestamp_server_start) {
// download is still running (presumably)
download_obj.percent_complete = getDownloadPercent(download_obj);
}
}
}
}
*/
function getDownloadPercent(download_obj) { Any file that starts with <video title> will be counted as part of the "bytes downloaded", which will
if (!download_obj.final_size) { be divided by the "total expected bytes."
if (fs.existsSync(download_obj.expected_json_path)) { */
const file_json = JSON.parse(fs.readFileSync(download_obj.expected_json_path, 'utf8')); // assume it's a playlist for logic reasons
let calculated_filesize = null; const fileNames = Array.isArray(download['fileNames']) ? download['fileNames']
if (file_json['format_id']) { : [path.format(path.parse(download['_filename'].substring(0, download['_filename'].length-4)))];
calculated_filesize = 0;
const formats_used = file_json['format_id'].split('+');
for (let i = 0; i < file_json['formats'].length; i++) { const resulting_file_size = download['filesize'];
if (formats_used.includes(file_json['formats'][i]['format_id'])) { let sum_size = 0;
calculated_filesize += file_json['formats'][i]['filesize']; let glob_str = '';
} glob(`{${fileNames.join(',')}, }*`, (err, files) => {
files.forEach(file => {
try {
const file_stats = fs.statSync(file);
if (file_stats && file_stats.size) {
sum_size += file_stats.size;
} }
} catch (e) {
} }
download_obj.final_size = calculated_filesize; });
} else { download['percent_complete'] = (sum_size/resulting_file_size * 100).toFixed(2);
console.log('could not find json file'); updateDownloads();
} });
}
if (fs.existsSync(download_obj.expected_path)) {
const stats = fs.statSync(download_obj.expected_path);
const size = stats.size;
return (size / download_obj.final_size)*100;
} else {
console.log('could not find file');
return 0;
}
} }
// youtube-dl functions // youtube-dl functions
@@ -1821,7 +1861,7 @@ app.post('/api/tomp3', optionalJwt, async function(req, res) {
const is_playlist = url.includes('playlist'); const is_playlist = url.includes('playlist');
let result_obj = null; let result_obj = null;
if (safeDownloadOverride || is_playlist || options.customQualityConfiguration || options.customArgs || options.maxBitrate) if (true || safeDownloadOverride || is_playlist || options.customQualityConfiguration || options.customArgs || options.maxBitrate)
result_obj = await downloadFileByURL_exec(url, 'audio', options, req.query.sessionID); result_obj = await downloadFileByURL_exec(url, 'audio', options, req.query.sessionID);
else else
result_obj = await downloadFileByURL_normal(url, 'audio', options, req.query.sessionID); result_obj = await downloadFileByURL_normal(url, 'audio', options, req.query.sessionID);
@@ -1833,6 +1873,7 @@ app.post('/api/tomp3', optionalJwt, async function(req, res) {
}); });
app.post('/api/tomp4', optionalJwt, async function(req, res) { app.post('/api/tomp4', optionalJwt, async function(req, res) {
req.setTimeout(0); // remove timeout in case of long videos
var url = req.body.url; var url = req.body.url;
var options = { var options = {
customArgs: req.body.customArgs, customArgs: req.body.customArgs,
@@ -1850,7 +1891,7 @@ app.post('/api/tomp4', optionalJwt, async function(req, res) {
const is_playlist = url.includes('playlist'); const is_playlist = url.includes('playlist');
let result_obj = null; let result_obj = null;
if (safeDownloadOverride || is_playlist || options.customQualityConfiguration || options.customArgs || options.selectedHeight || !url.includes('youtu')) if (true || safeDownloadOverride || is_playlist || options.customQualityConfiguration || options.customArgs || options.selectedHeight || !url.includes('youtu'))
result_obj = await downloadFileByURL_exec(url, 'video', options, req.query.sessionID); result_obj = await downloadFileByURL_exec(url, 'video', options, req.query.sessionID);
else else
result_obj = await downloadFileByURL_normal(url, 'video', options, req.query.sessionID); result_obj = await downloadFileByURL_normal(url, 'video', options, req.query.sessionID);
@@ -1878,6 +1919,14 @@ app.get('/api/getMp3s', optionalJwt, function(req, res) {
playlists = auth_api.getUserPlaylists(req.user.uid, 'audio'); playlists = auth_api.getUserPlaylists(req.user.uid, 'audio');
} }
mp3s = JSON.parse(JSON.stringify(mp3s));
// add thumbnails if present
mp3s.forEach(mp3 => {
if (mp3['thumbnailPath'] && fs.existsSync(mp3['thumbnailPath']))
mp3['thumbnailBlob'] = fs.readFileSync(mp3['thumbnailPath']);
});
res.send({ res.send({
mp3s: mp3s, mp3s: mp3s,
playlists: playlists playlists: playlists
@@ -1897,6 +1946,14 @@ app.get('/api/getMp4s', optionalJwt, function(req, res) {
playlists = auth_api.getUserPlaylists(req.user.uid, 'video'); playlists = auth_api.getUserPlaylists(req.user.uid, 'video');
} }
mp4s = JSON.parse(JSON.stringify(mp4s));
// add thumbnails if present
mp4s.forEach(mp4 => {
if (mp4['thumbnailPath'] && fs.existsSync(mp4['thumbnailPath']))
mp4['thumbnailBlob'] = fs.readFileSync(mp4['thumbnailPath']);
});
res.send({ res.send({
mp4s: mp4s, mp4s: mp4s,
playlists: playlists playlists: playlists
@@ -1981,6 +2038,14 @@ app.post('/api/getAllFiles', optionalJwt, function (req, res) {
files = files.concat(sub.videos); files = files.concat(sub.videos);
} }
files = JSON.parse(JSON.stringify(files));
// add thumbnails if present
files.forEach(file => {
if (file['thumbnailPath'] && fs.existsSync(file['thumbnailPath']))
file['thumbnailBlob'] = fs.readFileSync(file['thumbnailPath']);
});
res.send({ res.send({
files: files, files: files,
playlists: playlists playlists: playlists

View File

@@ -9,7 +9,9 @@
"path-video": "video/", "path-video": "video/",
"use_youtubedl_archive": false, "use_youtubedl_archive": false,
"custom_args": "", "custom_args": "",
"safe_download_override": false "safe_download_override": false,
"include_thumbnail": true,
"include_metadata": true
}, },
"Extra": { "Extra": {
"title_top": "YoutubeDL-Material", "title_top": "YoutubeDL-Material",

View File

@@ -186,7 +186,9 @@ DEFAULT_CONFIG = {
"path-video": "video/", "path-video": "video/",
"use_youtubedl_archive": false, "use_youtubedl_archive": false,
"custom_args": "", "custom_args": "",
"safe_download_override": false "safe_download_override": false,
"include_thumbnail": true,
"include_metadata": true
}, },
"Extra": { "Extra": {
"title_top": "YoutubeDL-Material", "title_top": "YoutubeDL-Material",

View File

@@ -30,6 +30,14 @@ let CONFIG_ITEMS = {
'key': 'ytdl_safe_download_override', 'key': 'ytdl_safe_download_override',
'path': 'YoutubeDLMaterial.Downloader.safe_download_override' 'path': 'YoutubeDLMaterial.Downloader.safe_download_override'
}, },
'ytdl_include_thumbnail': {
'key': 'ytdl_include_thumbnail',
'path': 'YoutubeDLMaterial.Downloader.include_thumbnail'
},
'ytdl_include_metadata': {
'key': 'ytdl_include_metadata',
'path': 'YoutubeDLMaterial.Downloader.include_metadata'
},
// Extra // Extra
'ytdl_title_top': { 'ytdl_title_top': {

View File

@@ -26,11 +26,8 @@ function registerFileDB(file_path, type, multiUserMode = null, sub = null) {
utils.fixVideoMetadataPerms(file_id, type, multiUserMode && multiUserMode.file_path); utils.fixVideoMetadataPerms(file_id, type, multiUserMode && multiUserMode.file_path);
// add additional info // add thumbnail path
file_object['uid'] = uuid(); file_object['thumbnailPath'] = utils.getDownloadedThumbnail(file_id, type, multiUserMode && multiUserMode.file_path);
file_object['registered'] = Date.now();
path_object = path.parse(file_object['path']);
file_object['path'] = path.format(path_object);
if (!sub) { if (!sub) {
if (multiUserMode) { if (multiUserMode) {
@@ -48,7 +45,13 @@ function registerFileDB(file_path, type, multiUserMode = null, sub = null) {
} }
} }
const file_uid = registerFileDBManual(db_path, file_object) const file_uid = registerFileDBManual(db_path, file_object);
// remove metadata JSON if needed
if (!config_api.getConfigItem('ytdl_include_metadata')) {
utils.deleteJSONFile(file_id, type, multiUserMode && multiUserMode.file_path)
}
return file_uid; return file_uid;
} }
@@ -165,6 +168,10 @@ async function importUnregisteredFiles() {
// add subscriptions to check list // add subscriptions to check list
for (let i = 0; i < subscriptions_to_check.length; i++) { for (let i = 0; i < subscriptions_to_check.length; i++) {
let subscription_to_check = subscriptions_to_check[i]; let subscription_to_check = subscriptions_to_check[i];
if (!subscription_to_check.name) {
// TODO: Remove subscription as it'll never complete
continue;
}
dirs_to_check.push({ dirs_to_check.push({
basePath: multi_user_mode ? path.join(usersFileFolder, subscription_to_check.user_uid, 'subscriptions', subscription_to_check.isPlaylist ? 'playlists/' : 'channels/', subscription_to_check.name) basePath: multi_user_mode ? path.join(usersFileFolder, subscription_to_check.user_uid, 'subscriptions', subscription_to_check.isPlaylist ? 'playlists/' : 'channels/', subscription_to_check.name)
: path.join(subscriptions_base_path, subscription_to_check.isPlaylist ? 'playlists/' : 'channels/', subscription_to_check.name), : path.join(subscriptions_base_path, subscription_to_check.isPlaylist ? 'playlists/' : 'channels/', subscription_to_check.name),

View File

@@ -1968,9 +1968,9 @@
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
}, },
"node-fetch": { "node-fetch": {
"version": "2.6.0", "version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
}, },
"node-id3": { "node-id3": {
"version": "0.1.16", "version": "0.1.16",

View File

@@ -37,12 +37,13 @@
"express": "^4.17.1", "express": "^4.17.1",
"fluent-ffmpeg": "^2.1.2", "fluent-ffmpeg": "^2.1.2",
"fs-extra": "^9.0.0", "fs-extra": "^9.0.0",
"glob": "^7.1.6",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"lowdb": "^1.0.0", "lowdb": "^1.0.0",
"md5": "^2.2.1", "md5": "^2.2.1",
"merge-files": "^0.1.2", "merge-files": "^0.1.2",
"multer": "^1.4.2", "multer": "^1.4.2",
"node-fetch": "^2.6.0", "node-fetch": "^2.6.1",
"node-id3": "^0.1.14", "node-id3": "^0.1.14",
"nodemon": "^2.0.2", "nodemon": "^2.0.2",
"passport": "^0.4.1", "passport": "^0.4.1",

View File

@@ -345,6 +345,10 @@ async function getVideosForSub(sub, user_uid = null) {
} }
} }
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
downloadConfig.push('--write-thumbnail');
}
// get videos // get videos
logger.verbose('Subscription: getting videos for subscription ' + sub.name); logger.verbose('Subscription: getting videos for subscription ' + sub.name);
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) { youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {

View File

@@ -28,7 +28,7 @@ function getDownloadedFilesByType(basePath, type) {
var located_files = recFindByExt(basePath, ext); var located_files = recFindByExt(basePath, ext);
for (let i = 0; i < located_files.length; i++) { for (let i = 0; i < located_files.length; i++) {
let file = located_files[i]; let file = located_files[i];
var file_path = path.basename(file); var file_path = file.substring(basePath.includes('\\') ? basePath.length+1 : basePath.length, file.length);
var stats = fs.statSync(file); var stats = fs.statSync(file);
@@ -88,6 +88,49 @@ function getJSONByType(type, name, customPath, openReadPerms = false) {
return type === 'audio' ? getJSONMp3(name, customPath, openReadPerms) : getJSONMp4(name, customPath, openReadPerms) return type === 'audio' ? getJSONMp3(name, customPath, openReadPerms) : getJSONMp4(name, customPath, openReadPerms)
} }
function getDownloadedThumbnail(name, type, customPath = null) {
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path') : config_api.getConfigItem('ytdl_video_folder_path');
let jpgPath = path.join(customPath, name + '.jpg');
let webpPath = path.join(customPath, name + '.webp');
let pngPath = path.join(customPath, name + '.png');
if (fs.existsSync(jpgPath))
return jpgPath;
else if (fs.existsSync(webpPath))
return webpPath;
else if (fs.existsSync(pngPath))
return pngPath;
else
return null;
}
function getExpectedFileSize(input_info_jsons) {
// treat single videos as arrays to have the file sizes checked/added to. makes the code cleaner
const info_jsons = Array.isArray(input_info_jsons) ? input_info_jsons : [input_info_jsons];
let expected_filesize = 0;
info_jsons.forEach(info_json => {
if (info_json['filesize']) {
expected_filesize += info_json['filesize'];
return;
}
const formats = info_json['format_id'].split('+');
let individual_expected_filesize = 0;
formats.forEach(format_id => {
info_json.formats.forEach(available_format => {
if (available_format.format_id === format_id && available_format.filesize) {
individual_expected_filesize += available_format.filesize;
}
});
});
expected_filesize += individual_expected_filesize;
});
return expected_filesize;
}
function fixVideoMetadataPerms(name, type, customPath = null) { function fixVideoMetadataPerms(name, type, customPath = null) {
if (is_windows) return; if (is_windows) return;
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path') if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path')
@@ -110,6 +153,19 @@ function fixVideoMetadataPerms(name, type, customPath = null) {
} }
} }
function deleteJSONFile(name, type, customPath = null) {
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path')
: config_api.getConfigItem('ytdl_video_folder_path');
const ext = type === 'audio' ? '.mp3' : '.mp4';
let json_path = path.join(customPath, name + '.info.json');
let alternate_json_path = path.join(customPath, name + ext + '.info.json');
if (fs.existsSync(json_path)) fs.unlinkSync(json_path);
if (fs.existsSync(alternate_json_path)) fs.unlinkSync(alternate_json_path);
}
function recFindByExt(base,ext,files,result) function recFindByExt(base,ext,files,result)
{ {
files = files || fs.readdirSync(base) files = files || fs.readdirSync(base)
@@ -153,7 +209,10 @@ module.exports = {
getJSONMp3: getJSONMp3, getJSONMp3: getJSONMp3,
getJSONMp4: getJSONMp4, getJSONMp4: getJSONMp4,
getTrueFileName: getTrueFileName, getTrueFileName: getTrueFileName,
getDownloadedThumbnail: getDownloadedThumbnail,
getExpectedFileSize: getExpectedFileSize,
fixVideoMetadataPerms: fixVideoMetadataPerms, fixVideoMetadataPerms: fixVideoMetadataPerms,
deleteJSONFile: deleteJSONFile,
getDownloadedFilesByType: getDownloadedFilesByType, getDownloadedFilesByType: getDownloadedFilesByType,
recFindByExt: recFindByExt, recFindByExt: recFindByExt,
File: File File: File

View File

@@ -1,3 +0,0 @@
#!/bin/bash
# downloads a local copy of qemu on docker-hub build machines
curl -L https://github.com/balena-io/qemu/releases/download/v3.0.0%2Bresin/qemu-3.0.0+resin-arm.tar.gz | tar zxvf - -C . && mv qemu-3.0.0+resin-arm/qemu-arm-static .

View File

@@ -1,4 +0,0 @@
#!/bin/bash
# Register qemu-*-static for all supported processors except the
# current one, but also remove all registered binfmt_misc before
docker run --rm --privileged multiarch/qemu-user-static:register --reset

View File

@@ -38,15 +38,15 @@
</div> </div>
<div class="sidenav-container" style="height: calc(100% - 64px)"> <div class="sidenav-container" style="height: calc(100% - 64px)">
<mat-sidenav-container style="height: 100%"> <mat-sidenav-container style="height: 100%">
<mat-sidenav [opened]="postsService.sidepanel_mode === 'side' && router.url === '/home'" [mode]="postsService.sidepanel_mode" #sidenav> <mat-sidenav [opened]="postsService.sidepanel_mode === 'side' && !window.location.href.includes('/player')" [mode]="postsService.sidepanel_mode" #sidenav>
<mat-nav-list> <mat-nav-list>
<a *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn)" mat-list-item (click)="sidenav.close()" routerLink='/home'><ng-container i18n="Navigation menu Home Page title">Home</ng-container></a> <a *ngIf="postsService.config && (!postsService.config.Advanced.multi_user_mode || postsService.isLoggedIn)" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/home'><ng-container i18n="Navigation menu Home Page title">Home</ng-container></a>
<a *ngIf="postsService.config && postsService.config.Advanced.multi_user_mode && !postsService.isLoggedIn" mat-list-item (click)="sidenav.close()" routerLink='/login'><ng-container i18n="Navigation menu Login Page title">Login</ng-container></a> <a *ngIf="postsService.config && postsService.config.Advanced.multi_user_mode && !postsService.isLoggedIn" mat-list-item (click)="sidenav.close()" routerLink='/login'><ng-container i18n="Navigation menu Login Page title">Login</ng-container></a>
<a *ngIf="postsService.config && allowSubscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))" mat-list-item (click)="sidenav.close()" routerLink='/subscriptions'><ng-container i18n="Navigation menu Subscriptions Page title">Subscriptions</ng-container></a> <a *ngIf="postsService.config && allowSubscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/subscriptions'><ng-container i18n="Navigation menu Subscriptions Page title">Subscriptions</ng-container></a>
<a *ngIf="postsService.config && enableDownloadsManager && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('downloads_manager')))" mat-list-item (click)="sidenav.close()" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a> <a *ngIf="postsService.config && enableDownloadsManager && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('downloads_manager')))" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/downloads'><ng-container i18n="Navigation menu Downloads Page title">Downloads</ng-container></a>
<ng-container *ngIf="postsService.config && allowSubscriptions && postsService.subscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))"> <ng-container *ngIf="postsService.config && allowSubscriptions && postsService.subscriptions && (!postsService.config.Advanced.multi_user_mode || (postsService.isLoggedIn && postsService.permissions.includes('subscriptions')))">
<mat-divider></mat-divider> <mat-divider></mat-divider>
<a *ngFor="let subscription of postsService.subscriptions" mat-list-item (click)="sidenav.close()" [routerLink]="['/subscription', { id: subscription.id }]"><ngx-avatar [style.margin-right]="'10px'" size="32" [name]="subscription.name"></ngx-avatar><ng-container i18n="Navigation menu Downloads Page title">{{subscription.name}}</ng-container></a> <a *ngFor="let subscription of postsService.subscriptions" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" [routerLink]="['/subscription', { id: subscription.id }]"><ngx-avatar [style.margin-right]="'10px'" size="32" [name]="subscription.name"></ngx-avatar><ng-container i18n="Navigation menu Downloads Page title">{{subscription.name}}</ng-container></a>
</ng-container> </ng-container>
</mat-nav-list> </mat-nav-list>
</mat-sidenav> </mat-sidenav>

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, ElementRef, ViewChild, HostBinding } from '@angular/core'; import { Component, OnInit, ElementRef, ViewChild, HostBinding, AfterViewInit } from '@angular/core';
import {PostsService} from './posts.services'; import {PostsService} from './posts.services';
import {FileCardComponent} from './file-card/file-card.component'; import {FileCardComponent} from './file-card/file-card.component';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
@@ -30,11 +30,13 @@ import { SetDefaultAdminDialogComponent } from './dialogs/set-default-admin-dial
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.css'] styleUrls: ['./app.component.css']
}) })
export class AppComponent implements OnInit { export class AppComponent implements OnInit, AfterViewInit {
@HostBinding('class') componentCssClass; @HostBinding('class') componentCssClass;
THEMES_CONFIG = THEMES_CONFIG; THEMES_CONFIG = THEMES_CONFIG;
window = window;
// config items // config items
topBarTitle = 'Youtube Downloader'; topBarTitle = 'Youtube Downloader';
defaultTheme = null; defaultTheme = null;
@@ -69,6 +71,29 @@ export class AppComponent implements OnInit {
} }
ngOnInit() {
if (localStorage.getItem('theme')) {
this.setTheme(localStorage.getItem('theme'));
}
this.postsService.open_create_default_admin_dialog.subscribe(open => {
if (open) {
const dialogRef = this.dialog.open(SetDefaultAdminDialogComponent);
dialogRef.afterClosed().subscribe(success => {
if (success) {
if (this.router.url !== '/login') { this.router.navigate(['/login']); }
} else {
console.error('Failed to create default admin account. See logs for details.');
}
});
}
});
}
ngAfterViewInit() {
this.postsService.sidenav = this.sidenav;
}
toggleSidenav() { toggleSidenav() {
this.sidenav.toggle(); this.sidenav.toggle();
} }
@@ -89,9 +114,7 @@ export class AppComponent implements OnInit {
// gets the subscriptions // gets the subscriptions
if (this.allowSubscriptions) { if (this.allowSubscriptions) {
this.postsService.getAllSubscriptions().subscribe(res => { this.postsService.reloadSubscriptions();
this.postsService.subscriptions = res['subscriptions'];
})
} }
} }
@@ -124,9 +147,9 @@ export class AppComponent implements OnInit {
this.postsService.setTheme(theme); this.postsService.setTheme(theme);
this.onSetTheme(this.THEMES_CONFIG[theme]['css_label'], old_theme ? this.THEMES_CONFIG[old_theme]['css_label'] : old_theme); this.onSetTheme(this.THEMES_CONFIG[theme]['css_label'], old_theme ? this.THEMES_CONFIG[old_theme]['css_label'] : old_theme);
} }
onSetTheme(theme, old_theme) { onSetTheme(theme, old_theme) {
if (old_theme) { if (old_theme) {
document.body.classList.remove(old_theme); document.body.classList.remove(old_theme);
this.overlayContainer.getContainerElement().classList.remove(old_theme); this.overlayContainer.getContainerElement().classList.remove(old_theme);
@@ -148,27 +171,6 @@ onSetTheme(theme, old_theme) {
event.stopPropagation(); event.stopPropagation();
} }
ngOnInit() {
if (localStorage.getItem('theme')) {
this.setTheme(localStorage.getItem('theme'));
} else {
//
}
this.postsService.open_create_default_admin_dialog.subscribe(open => {
if (open) {
const dialogRef = this.dialog.open(SetDefaultAdminDialogComponent);
dialogRef.afterClosed().subscribe(success => {
if (success) {
if (this.router.url !== '/login') { this.router.navigate(['/login']); }
} else {
console.error('Failed to create default admin account. See logs for details.');
}
});
}
});
}
getSubscriptions() { getSubscriptions() {
} }

View File

@@ -50,7 +50,8 @@ export class CustomPlaylistsComponent implements OnInit {
}); });
} }
goToPlaylist(playlist) { goToPlaylist(info_obj) {
const playlist = info_obj.file;
const playlistID = playlist.id; const playlistID = playlist.id;
const type = playlist.type; const type = playlist.type;

View File

@@ -31,12 +31,12 @@
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<ng-container *ngIf="normal_files_received"> <ng-container *ngIf="normal_files_received">
<div *ngFor="let file of filtered_files; let i = index" class="mb-2 mt-2" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]"> <div *ngFor="let file of filtered_files; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" (goToFile)="goToFile($event)" (goToSubscription)="goToSubscription($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" [loading]="false" (deleteFile)="deleteFile($event)"></app-unified-file-card> <app-unified-file-card [index]="i" [card_size]="postsService.card_size" (goToFile)="goToFile($event)" (goToSubscription)="goToSubscription($event)" [file_obj]="file" [use_youtubedl_archive]="postsService.config['Downloader']['use_youtubedl_archive']" [loading]="false" (deleteFile)="deleteFile($event)"></app-unified-file-card>
</div> </div>
</ng-container> </ng-container>
<ng-container *ngIf="!normal_files_received && loading_files && loading_files.length > 0"> <ng-container *ngIf="!normal_files_received && loading_files && loading_files.length > 0">
<div *ngFor="let file of loading_files; let i = index" class="mb-2 mt-2" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]"> <div *ngFor="let file of loading_files; let i = index" class="mb-2 mt-2 d-flex justify-content-center" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [loading]="true" [theme]="postsService.theme"></app-unified-file-card> <app-unified-file-card [index]="i" [card_size]="postsService.card_size" [loading]="true" [theme]="postsService.theme"></app-unified-file-card>
</div> </div>
</ng-container> </ng-container>

View File

@@ -55,17 +55,21 @@ export class RecentVideosComponent implements OnInit {
if (localStorage.getItem('cached_file_count')) { if (localStorage.getItem('cached_file_count')) {
this.cached_file_count = +localStorage.getItem('cached_file_count'); this.cached_file_count = +localStorage.getItem('cached_file_count');
this.loading_files = Array(this.cached_file_count).fill(0); this.loading_files = Array(this.cached_file_count).fill(0);
console.log(this.loading_files);
} }
} }
ngOnInit(): void { ngOnInit(): void {
if (this.postsService.initialized) {
this.getAllFiles();
}
this.postsService.service_initialized.subscribe(init => { this.postsService.service_initialized.subscribe(init => {
if (init) { if (init) {
this.getAllFiles(); this.getAllFiles();
} }
}); });
// set filter property to cached // set filter property to cached
const cached_filter_property = localStorage.getItem('filter_property'); const cached_filter_property = localStorage.getItem('filter_property');
if (cached_filter_property && this.filterProperties[cached_filter_property]) { if (cached_filter_property && this.filterProperties[cached_filter_property]) {
@@ -134,28 +138,36 @@ export class RecentVideosComponent implements OnInit {
// navigation // navigation
goToFile(file) { goToFile(info_obj) {
const file = info_obj['file'];
const event = info_obj['event'];
if (this.postsService.config['Extra']['download_only_mode']) { if (this.postsService.config['Extra']['download_only_mode']) {
this.downloadFile(file); this.downloadFile(file);
} else { } else {
this.navigateToFile(file); this.navigateToFile(file, event.ctrlKey);
} }
} }
navigateToFile(file) { navigateToFile(file, new_tab) {
localStorage.setItem('player_navigator', this.router.url); localStorage.setItem('player_navigator', this.router.url);
if (file.sub_id) { if (file.sub_id) {
const sub = this.postsService.getSubscriptionByID(file.sub_id) const sub = this.postsService.getSubscriptionByID(file.sub_id);
if (sub.streamingOnly) { if (sub.streamingOnly) {
this.router.navigate(['/player', {name: file.id, // streaming only mode subscriptions
url: file.requested_formats ? file.requested_formats[0].url : file.url}]); !new_tab ? this.router.navigate(['/player', {name: file.id,
url: file.requested_formats ? file.requested_formats[0].url : file.url}])
: window.open(`/#/player;name=${file.id};url=${file.requested_formats ? file.requested_formats[0].url : file.url}`);
} else { } else {
this.router.navigate(['/player', {fileNames: file.id, // normal subscriptions
type: file.isAudio ? 'audio' : 'video', subscriptionName: sub.name, !new_tab ? this.router.navigate(['/player', {fileNames: file.id,
subPlaylist: sub.isPlaylist}]); type: file.isAudio ? 'audio' : 'video', subscriptionName: sub.name,
subPlaylist: sub.isPlaylist}])
: window.open(`/#/player;fileNames=${file.id};type=${file.isAudio ? 'audio' : 'video'};subscriptionName=${sub.name};subPlaylist=${sub.isPlaylist}`);
} }
} else { } else {
this.router.navigate(['/player', {type: file.isAudio ? 'audio' : 'video', uid: file.uid}]); // normal files
!new_tab ? this.router.navigate(['/player', {type: file.isAudio ? 'audio' : 'video', uid: file.uid}])
: window.open(`/#/player;type=${file.isAudio ? 'audio' : 'video'};uid=${file.uid}`);
} }
} }
@@ -177,7 +189,6 @@ export class RecentVideosComponent implements OnInit {
const type = file.isAudio ? 'audio' : 'video'; const type = file.isAudio ? 'audio' : 'video';
const ext = type === 'audio' ? '.mp3' : '.mp4' const ext = type === 'audio' ? '.mp3' : '.mp4'
const sub = this.postsService.getSubscriptionByID(file.sub_id); const sub = this.postsService.getSubscriptionByID(file.sub_id);
console.log(sub.isPlaylist)
this.postsService.downloadFileFromServer(file.id, type, null, null, sub.name, sub.isPlaylist, this.postsService.downloadFileFromServer(file.id, type, null, null, sub.name, sub.isPlaylist,
this.postsService.user ? this.postsService.user.uid : null, null).subscribe(res => { this.postsService.user ? this.postsService.user.uid : null, null).subscribe(res => {
const blob: Blob = res; const blob: Blob = res;

View File

@@ -25,11 +25,11 @@
<button mat-menu-item>Placeholder</button> <button mat-menu-item>Placeholder</button>
</ng-container> </ng-container>
</mat-menu> </mat-menu>
<mat-card [matTooltip]="null" (click)="navigateToFile()" matRipple class="file-mat-card" [ngClass]="{'small-mat-card': card_size === 'small', 'file-mat-card': card_size === 'medium', 'large-mat-card': card_size === 'large', 'mat-elevation-z4': !elevated, 'mat-elevation-z8': elevated}"> <mat-card [matTooltip]="null" (click)="navigateToFile($event)" matRipple class="file-mat-card" [ngClass]="{'small-mat-card': card_size === 'small', 'file-mat-card': card_size === 'medium', 'large-mat-card': card_size === 'large', 'mat-elevation-z4': !elevated, 'mat-elevation-z8': elevated}">
<div style="padding:5px"> <div style="padding:5px">
<div *ngIf="!loading && file_obj.thumbnailURL" class="img-div"> <div *ngIf="!loading && file_obj.thumbnailURL" class="img-div">
<div style="position: relative"> <div style="position: relative">
<img [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" [src]="file_obj.thumbnailURL" alt="Thumbnail"> <img [ngClass]="{'image-small': card_size === 'small', 'image': card_size === 'medium', 'image-large': card_size === 'large'}" [src]="file_obj.thumbnailBlob ? thumbnailBlobURL : file_obj.thumbnailURL" alt="Thumbnail">
<div class="duration-time"> <div class="duration-time">
{{file_length}} {{file_length}}
</div> </div>

View File

@@ -103,6 +103,7 @@
background: rgba(255,255,255,0.6); background: rgba(255,255,255,0.6);
padding-left: 5px; padding-left: 5px;
padding-right: 5px; padding-right: 5px;
color: black;
} }
.download-time { .download-time {

View File

@@ -1,6 +1,7 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component'; import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component';
import { DomSanitizer } from '@angular/platform-browser';
@Component({ @Component({
selector: 'app-unified-file-card', selector: 'app-unified-file-card',
@@ -16,6 +17,10 @@ export class UnifiedFileCardComponent implements OnInit {
type = null; type = null;
elevated = false; elevated = false;
// optional vars
thumbnailBlobURL = null;
// input/output
@Input() loading = true; @Input() loading = true;
@Input() theme = null; @Input() theme = null;
@Input() file_obj = null; @Input() file_obj = null;
@@ -35,12 +40,19 @@ export class UnifiedFileCardComponent implements OnInit {
big: 250x200 big: 250x200
*/ */
constructor(private dialog: MatDialog) { } constructor(private dialog: MatDialog, private sanitizer: DomSanitizer) { }
ngOnInit(): void { ngOnInit(): void {
if (!this.loading) { if (!this.loading) {
this.file_length = fancyTimeFormat(this.file_obj.duration); this.file_length = fancyTimeFormat(this.file_obj.duration);
} }
if (this.file_obj && this.file_obj.thumbnailBlob) {
const mime = getMimeByFilename(this.file_obj.thumbnailPath);
const blob = new Blob([new Uint8Array(this.file_obj.thumbnailBlob.data)], {type: mime});
const bloburl = URL.createObjectURL(blob);
this.thumbnailBlobURL = this.sanitizer.bypassSecurityTrustUrl(bloburl);
}
} }
emitDeleteFile(blacklistMode = false) { emitDeleteFile(blacklistMode = false) {
@@ -51,8 +63,8 @@ export class UnifiedFileCardComponent implements OnInit {
}); });
} }
navigateToFile() { navigateToFile(event) {
this.goToFile.emit(this.file_obj); this.goToFile.emit({file: this.file_obj, event: event});
} }
navigateToSubscription() { navigateToSubscription() {
@@ -97,3 +109,16 @@ function fancyTimeFormat(time) {
ret += '' + secs; ret += '' + secs;
return ret; return ret;
} }
function getMimeByFilename(name) {
switch (name.substring(name.length-4, name.length)) {
case '.jpg':
return 'image/jpeg';
case '.png':
return 'image/png';
case 'webp':
return 'image/webp';
default:
return null;
}
}

View File

@@ -164,7 +164,7 @@
<br/> <br/>
<div class="centered big" id="bar_div" *ngIf="current_download && current_download.downloading; else nofile"> <div class="centered big" id="bar_div" *ngIf="current_download && current_download.downloading; else nofile">
<div class="margined"> <div class="margined">
<div [ngClass]="(+percentDownloaded > 99)?'make-room-for-spinner':'equal-sizes'" style="display: inline-block; width: 100%; padding-left: 20px" *ngIf="current_download.percent_complete && current_download.percent_complete > 15;else indeterminateprogress"> <div [ngClass]="(+percentDownloaded > 99)?'make-room-for-spinner':'equal-sizes'" style="display: inline-block; width: 100%; padding-left: 20px" *ngIf="current_download.percent_complete && current_download.percent_complete > 1;else indeterminateprogress">
<mat-progress-bar style="border-radius: 5px;" mode="determinate" value="{{percentDownloaded}}"></mat-progress-bar> <mat-progress-bar style="border-radius: 5px;" mode="determinate" value="{{percentDownloaded}}"></mat-progress-bar>
<br/> <br/>
</div> </div>
@@ -181,10 +181,12 @@
</ng-template> </ng-template>
<app-recent-videos></app-recent-videos> <ng-container *ngIf="cachedFileManagerEnabled || fileManagerEnabled">
<br/> <app-recent-videos></app-recent-videos>
<h4 style="text-align: center">Custom playlists</h4> <br/>
<app-custom-playlists></app-custom-playlists> <h4 style="text-align: center">Custom playlists</h4>
<app-custom-playlists></app-custom-playlists>
</ng-container>
<!--<div style="margin: 20px" *ngIf="fileManagerEnabled && (!postsService.isLoggedIn || postsService.permissions.includes('filemanager'))"> <!--<div style="margin: 20px" *ngIf="fileManagerEnabled && (!postsService.isLoggedIn || postsService.permissions.includes('filemanager'))">
<mat-accordion> <mat-accordion>

View File

@@ -82,8 +82,9 @@ export class MainComponent implements OnInit {
useDefaultDownloadingAgent = true; useDefaultDownloadingAgent = true;
customDownloadingAgent = null; customDownloadingAgent = null;
// formats cache // cache
cachedAvailableFormats = {}; cachedAvailableFormats = {};
cachedFileManagerEnabled = localStorage.getItem('cached_filemanager_enabled') === 'true';
// youtube api // youtube api
youtubeSearchEnabled = false; youtubeSearchEnabled = false;
@@ -232,7 +233,8 @@ export class MainComponent implements OnInit {
async loadConfig() { async loadConfig() {
// loading config // loading config
this.fileManagerEnabled = this.postsService.config['Extra']['file_manager_enabled']; this.fileManagerEnabled = this.postsService.config['Extra']['file_manager_enabled']
&& (!this.postsService.isLoggedIn || this.postsService.permissions.includes('filemanager'));
this.downloadOnlyMode = this.postsService.config['Extra']['download_only_mode']; this.downloadOnlyMode = this.postsService.config['Extra']['download_only_mode'];
this.allowMultiDownloadMode = this.postsService.config['Extra']['allow_multi_download_mode']; this.allowMultiDownloadMode = this.postsService.config['Extra']['allow_multi_download_mode'];
this.audioFolderPath = this.postsService.config['Downloader']['path-audio']; this.audioFolderPath = this.postsService.config['Downloader']['path-audio'];
@@ -261,6 +263,10 @@ export class MainComponent implements OnInit {
} }
// set final cache items // set final cache items
localStorage.setItem('cached_filemanager_enabled', this.fileManagerEnabled.toString());
this.cachedFileManagerEnabled = this.fileManagerEnabled;
if (this.allowAdvancedDownload) { if (this.allowAdvancedDownload) {
if (localStorage.getItem('customArgsEnabled') !== null) { if (localStorage.getItem('customArgsEnabled') !== null) {
this.customArgsEnabled = localStorage.getItem('customArgsEnabled') === 'true'; this.customArgsEnabled = localStorage.getItem('customArgsEnabled') === 'true';
@@ -1033,8 +1039,8 @@ export class MainComponent implements OnInit {
} }
} else if (format_obj.type === 'video') { } else if (format_obj.type === 'video') {
// check if video format is mp4 // check if video format is mp4
const key = format.height.toString(); const key = format.format_note.replace('p', '');
if (format.ext === 'mp4') { if (format.ext === 'mp4' || format.ext === 'mkv' || format.ext === 'webm') {
format_obj['height'] = format.height; format_obj['height'] = format.height;
format_obj['acodec'] = format.acodec; format_obj['acodec'] = format.acodec;
format_obj['format_id'] = format.format_id; format_obj['format_id'] = format.format_id;

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, HostListener, EventEmitter, OnDestroy } from '@angular/core'; import { Component, OnInit, HostListener, EventEmitter, OnDestroy, AfterViewInit } from '@angular/core';
import { VgAPI } from 'ngx-videogular'; import { VgAPI } from 'ngx-videogular';
import { PostsService } from 'app/posts.services'; import { PostsService } from 'app/posts.services';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
@@ -20,7 +20,7 @@ export interface IMedia {
templateUrl: './player.component.html', templateUrl: './player.component.html',
styleUrls: ['./player.component.css'] styleUrls: ['./player.component.css']
}) })
export class PlayerComponent implements OnInit, OnDestroy { export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
playlist: Array<IMedia> = []; playlist: Array<IMedia> = [];
original_playlist: string = null; original_playlist: string = null;
@@ -95,6 +95,10 @@ export class PlayerComponent implements OnInit, OnDestroy {
} }
} }
ngAfterViewInit() {
this.postsService.sidenav.close();
}
ngOnDestroy() { ngOnDestroy() {
// prevents volume save feature from running in the background // prevents volume save feature from running in the background
clearInterval(this.save_volume_timer); clearInterval(this.save_volume_timer);

View File

@@ -15,16 +15,14 @@ import * as Fingerprint2 from 'fingerprintjs2';
@Injectable() @Injectable()
export class PostsService implements CanActivate { export class PostsService implements CanActivate {
path = ''; path = '';
audioFolder = '';
videoFolder = ''; // local settings
startPath = null; // 'http://localhost:17442/';
startPathSSL = null; // 'https://localhost:17442/'
handShakeComplete = false;
THEMES_CONFIG = THEMES_CONFIG; THEMES_CONFIG = THEMES_CONFIG;
theme; theme;
card_size = 'medium'; card_size = 'medium';
sidepanel_mode = 'over'; sidepanel_mode = 'over';
settings_changed = new BehaviorSubject<boolean>(false);
// auth
auth_token = '4241b401-7236-493e-92b5-b72696b9d853'; auth_token = '4241b401-7236-493e-92b5-b72696b9d853';
session_id = null; session_id = null;
httpOptions = null; httpOptions = null;
@@ -41,20 +39,24 @@ export class PostsService implements CanActivate {
available_permissions = null; available_permissions = null;
// behavior subjects
reload_config = new BehaviorSubject<boolean>(false); reload_config = new BehaviorSubject<boolean>(false);
config_reloaded = new BehaviorSubject<boolean>(false); config_reloaded = new BehaviorSubject<boolean>(false);
service_initialized = new BehaviorSubject<boolean>(false); service_initialized = new BehaviorSubject<boolean>(false);
initialized = false; settings_changed = new BehaviorSubject<boolean>(false);
open_create_default_admin_dialog = new BehaviorSubject<boolean>(false); open_create_default_admin_dialog = new BehaviorSubject<boolean>(false);
// app status
initialized = false;
// global vars
config = null; config = null;
subscriptions = null; subscriptions = null;
sidenav = null;
constructor(private http: HttpClient, private router: Router, @Inject(DOCUMENT) private document: Document, constructor(private http: HttpClient, private router: Router, @Inject(DOCUMENT) private document: Document,
public snackBar: MatSnackBar) { public snackBar: MatSnackBar) {
console.log('PostsService Initialized...'); console.log('PostsService Initialized...');
// this.startPath = window.location.href + '/api/';
// this.startPathSSL = window.location.href + '/api/';
this.path = this.document.location.origin + '/api/'; this.path = this.document.location.origin + '/api/';
if (isDevMode()) { if (isDevMode()) {
@@ -152,14 +154,6 @@ export class PostsService implements CanActivate {
}); });
} }
getVideoFolder() {
return this.http.get(this.startPath + 'videofolder');
}
getAudioFolder() {
return this.http.get(this.startPath + 'audiofolder');
}
// tslint:disable-next-line: max-line-length // tslint:disable-next-line: max-line-length
makeMP3(url: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, customOutput: string = null, youtubeUsername: string = null, youtubePassword: string = null, ui_uid = null) { makeMP3(url: string, selectedQuality: string, customQualityConfiguration: string, customArgs: string = null, customOutput: string = null, youtubeUsername: string = null, youtubePassword: string = null, ui_uid = null) {
return this.http.post(this.path + 'tomp3', {url: url, return this.http.post(this.path + 'tomp3', {url: url,
@@ -398,6 +392,8 @@ export class PostsService implements CanActivate {
}, err => { }, err => {
if (err.status === 401) { if (err.status === 401) {
this.sendToLogin(); this.sendToLogin();
this.token = null;
this.resetHttpParams();
} }
console.log(err); console.log(err);
}); });
@@ -414,14 +410,7 @@ export class PostsService implements CanActivate {
this.router.navigate(['/login']); this.router.navigate(['/login']);
} }
// resets http params this.resetHttpParams();
this.http_params = `apiKey=${this.auth_token}&sessionID=${this.session_id}`
this.httpOptions = {
params: new HttpParams({
fromString: this.http_params
}),
};
} }
// user methods // user methods
@@ -446,12 +435,29 @@ export class PostsService implements CanActivate {
this.openSnackBar('You must log in to access this page!'); this.openSnackBar('You must log in to access this page!');
} }
resetHttpParams() {
// resets http params
this.http_params = `apiKey=${this.auth_token}&sessionID=${this.session_id}`
this.httpOptions = {
params: new HttpParams({
fromString: this.http_params
}),
};
}
setInitialized() { setInitialized() {
this.service_initialized.next(true); this.service_initialized.next(true);
this.initialized = true; this.initialized = true;
this.config_reloaded.next(true); this.config_reloaded.next(true);
} }
reloadSubscriptions() {
this.getAllSubscriptions().subscribe(res => {
this.subscriptions = res['subscriptions'];
});
}
adminExists() { adminExists() {
return this.http.post(this.path + 'auth/adminExists', {}, this.httpOptions); return this.http.post(this.path + 'auth/adminExists', {}, this.httpOptions);
} }

View File

@@ -128,7 +128,11 @@
</div> </div>
<div class="col-12 mt-2"> <div class="col-12 mt-2">
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['safe_download_override']"><ng-container i18n="Safe download override setting">Safe download override</ng-container></mat-checkbox> <mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['include_thumbnail']"><ng-container i18n="Include thumbnail setting">Include thumbnail</ng-container></mat-checkbox>
</div>
<div class="col-12 mt-2">
<mat-checkbox color="accent" [(ngModel)]="new_config['Downloader']['include_metadata']"><ng-container i18n="Include metadata setting">Include metadata</ng-container></mat-checkbox>
</div> </div>
<div class="col-12 mt-2"> <div class="col-12 mt-2">

View File

@@ -80,6 +80,7 @@ export class SubscriptionsComponent implements OnInit {
} else { } else {
this.channel_subscriptions.push(result); this.channel_subscriptions.push(result);
} }
this.postsService.reloadSubscriptions();
} }
}); });
} }
@@ -96,6 +97,7 @@ export class SubscriptionsComponent implements OnInit {
if (success) { if (success) {
this.openSnackBar(`${sub.name} successfully deleted!`) this.openSnackBar(`${sub.name} successfully deleted!`)
this.getSubscriptions(); this.getSubscriptions();
this.postsService.reloadSubscriptions();
} }
}) })
} }