mirror of
https://github.com/Tzahi12345/YoutubeDL-Material.git
synced 2026-03-08 04:20:08 +03:00
Compare commits
32 Commits
add-ldap-a
...
blacklist-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f143d449b | ||
|
|
899633e124 | ||
|
|
8fdc231f08 | ||
|
|
ae8f7a2a33 | ||
|
|
d0782bb444 | ||
|
|
49210abb49 | ||
|
|
851bfb81ba | ||
|
|
35d0d439fa | ||
|
|
ded3ad6dfc | ||
|
|
61daf26641 | ||
|
|
95e53b9549 | ||
|
|
46ed0fe992 | ||
|
|
082252ab1e | ||
|
|
5eccaa13e5 | ||
|
|
71633950b2 | ||
|
|
f31dad0215 | ||
|
|
7efbe40bb2 | ||
|
|
5b768b5bda | ||
|
|
365cbc3ffa | ||
|
|
44647f3306 | ||
|
|
8a7409478a | ||
|
|
70159813e5 | ||
|
|
d292275956 | ||
|
|
ba2acedb94 | ||
|
|
aa0558b770 | ||
|
|
d7f04fc90a | ||
|
|
087c9f1bb1 | ||
|
|
f874617965 | ||
|
|
8fb8543829 | ||
|
|
70d89d310c | ||
|
|
c48aaaf13c | ||
|
|
6cf7ea193a |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -65,3 +65,4 @@ backend/appdata/logs/error.log
|
||||
backend/appdata/users.json
|
||||
backend/users/*
|
||||
backend/appdata/cookies.txt
|
||||
backend/public
|
||||
@@ -90,7 +90,7 @@ environment:
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
FROM arm32v7/alpine:3.12 as frontend
|
||||
FROM alpine:3.12 as frontend
|
||||
|
||||
RUN apk add --no-cache \
|
||||
npm
|
||||
npm \
|
||||
curl
|
||||
|
||||
RUN npm install -g @angular/cli
|
||||
|
||||
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/" ]
|
||||
RUN npm install
|
||||
|
||||
@@ -17,7 +21,7 @@ RUN ng build --prod
|
||||
|
||||
FROM arm32v7/alpine:3.12
|
||||
|
||||
COPY qemu-arm-static /usr/bin
|
||||
COPY --from=frontend /build/qemu-arm-static /usr/bin
|
||||
|
||||
ENV UID=1000 \
|
||||
GID=1000 \
|
||||
|
||||
159
backend/app.js
159
backend/app.js
@@ -7,7 +7,7 @@ var path = require('path');
|
||||
var youtubedl = require('youtube-dl');
|
||||
var ffmpeg = require('fluent-ffmpeg');
|
||||
var compression = require('compression');
|
||||
var https = require('https');
|
||||
var glob = require("glob")
|
||||
var multer = require('multer');
|
||||
var express = require("express");
|
||||
var bodyParser = require("body-parser");
|
||||
@@ -1146,12 +1146,29 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
|
||||
type: type,
|
||||
percent_complete: 0,
|
||||
is_playlist: url.includes('playlist'),
|
||||
timestamp_start: Date.now()
|
||||
timestamp_start: Date.now(),
|
||||
filesize: null
|
||||
};
|
||||
const download = downloads[session][download_uid];
|
||||
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
|
||||
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) {
|
||||
clearInterval(download_checker); // stops the download checker from running as the download finished (or errored)
|
||||
|
||||
download['downloading'] = false;
|
||||
download['timestamp_end'] = Date.now();
|
||||
var file_uid = null;
|
||||
@@ -1164,7 +1181,7 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
|
||||
download['error'] = err.stderr;
|
||||
updateDownloads();
|
||||
resolve(false);
|
||||
throw err;
|
||||
return;
|
||||
} else if (output) {
|
||||
if (output.length === 0 || output[0].length === 0) {
|
||||
download['error'] = 'No output. Check if video already exists in your archive.';
|
||||
@@ -1222,7 +1239,7 @@ async function downloadFileByURL_exec(url, type, options, sessionID = null) {
|
||||
if (options.merged_string !== null && options.merged_string !== undefined) {
|
||||
let current_merged_archive = fs.readFileSync(path.join(fileFolderPath, `merged_${type}.txt`), 'utf8');
|
||||
let diff = current_merged_archive.replace(options.merged_string, '');
|
||||
const archive_path = options.user ? path.join(fileFolderPath, 'archives', `archive_${type}.txt`) : path.join(archivePath, `archive_${type}.txt`);
|
||||
const archive_path = options.user ? path.join(config_api.getConfigItem('ytdl_users_base_path'), options.user, 'archives', `archive_${type}.txt`) : path.join(archivePath, `archive_${type}.txt`);
|
||||
fs.appendFileSync(archive_path, diff);
|
||||
}
|
||||
|
||||
@@ -1357,7 +1374,7 @@ async function downloadFileByURL_normal(url, type, options, sessionID = null) {
|
||||
if (options.merged_string !== null && options.merged_string !== undefined) {
|
||||
let current_merged_archive = fs.readFileSync(path.join(fileFolderPath, `merged_${type}.txt`), 'utf8');
|
||||
let diff = current_merged_archive.replace(options.merged_string, '');
|
||||
const archive_path = options.user ? path.join(fileFolderPath, 'archives', `archive_${type}.txt`) : path.join(archivePath, `archive_${type}.txt`);
|
||||
const archive_path = options.user ? path.join(config_api.getConfigItem('ytdl_users_base_path'), options.user, 'archives', `archive_${type}.txt`) : path.join(archivePath, `archive_${type}.txt`);
|
||||
fs.appendFileSync(archive_path, diff);
|
||||
}
|
||||
|
||||
@@ -1407,7 +1424,7 @@ async function generateArgs(url, type, options) {
|
||||
var youtubePassword = options.youtubePassword;
|
||||
|
||||
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');
|
||||
if (!is_audio && !is_youtube) {
|
||||
// tiktok videos fail when using the default format
|
||||
@@ -1458,7 +1475,7 @@ async function generateArgs(url, type, options) {
|
||||
|
||||
let useYoutubeDLArchive = config_api.getConfigItem('ytdl_use_youtubedl_archive');
|
||||
if (useYoutubeDLArchive) {
|
||||
const archive_folder = options.user ? path.join(fileFolderPath, 'archives') : archivePath;
|
||||
const archive_folder = options.user ? path.join(config_api.getConfigItem('ytdl_users_base_path'), options.user, 'archives') : archivePath;
|
||||
const archive_path = path.join(archive_folder, `archive_${type}.txt`);
|
||||
|
||||
fs.ensureDirSync(archive_folder);
|
||||
@@ -1468,7 +1485,7 @@ async function generateArgs(url, type, options) {
|
||||
fs.closeSync(fs.openSync(archive_path, 'w'));
|
||||
}
|
||||
|
||||
let blacklist_path = options.user ? path.join(fileFolderPath, 'archives', `blacklist_${type}.txt`) : path.join(archivePath, `blacklist_${type}.txt`);
|
||||
let blacklist_path = options.user ? path.join(config_api.getConfigItem('ytdl_users_base_path'), options.user, 'archives', `blacklist_${type}.txt`) : path.join(archivePath, `blacklist_${type}.txt`);
|
||||
// create blacklist file if it doesn't exist
|
||||
if (!fs.existsSync(blacklist_path)) {
|
||||
fs.closeSync(fs.openSync(blacklist_path, 'w'));
|
||||
@@ -1485,6 +1502,10 @@ async function generateArgs(url, type, options) {
|
||||
downloadConfig.push('--download-archive', merged_path);
|
||||
}
|
||||
|
||||
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
|
||||
downloadConfig.push('--write-thumbnail');
|
||||
}
|
||||
|
||||
if (globalArgs && globalArgs !== '') {
|
||||
// adds global args
|
||||
if (downloadConfig.indexOf('-o') !== -1 && globalArgs.split(',,').indexOf('-o') !== -1) {
|
||||
@@ -1497,11 +1518,36 @@ async function generateArgs(url, type, options) {
|
||||
|
||||
}
|
||||
logger.verbose(`youtube-dl args being used: ${downloadConfig.join(',')}`);
|
||||
// downloadConfig.map((arg) => `"${arg}"`);
|
||||
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
|
||||
async function getUrlInfos(urls) {
|
||||
let startDate = Date.now();
|
||||
@@ -1559,47 +1605,33 @@ function updateDownloads() {
|
||||
db.assign({downloads: downloads}).write();
|
||||
}
|
||||
|
||||
/*
|
||||
function checkDownloads() {
|
||||
for (let [session_id, session_downloads] of Object.entries(downloads)) {
|
||||
for (let [download_uid, download_obj] of Object.entries(session_downloads)) {
|
||||
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 checkDownloadPercent(download) {
|
||||
/*
|
||||
This is more of an art than a science, we're just selecting files that start with the file name,
|
||||
thus capturing the parts being downloaded in files named like so: '<video title>.<format>.<ext>.part'.
|
||||
|
||||
function getDownloadPercent(download_obj) {
|
||||
if (!download_obj.final_size) {
|
||||
if (fs.existsSync(download_obj.expected_json_path)) {
|
||||
const file_json = JSON.parse(fs.readFileSync(download_obj.expected_json_path, 'utf8'));
|
||||
let calculated_filesize = null;
|
||||
if (file_json['format_id']) {
|
||||
calculated_filesize = 0;
|
||||
const formats_used = file_json['format_id'].split('+');
|
||||
for (let i = 0; i < file_json['formats'].length; i++) {
|
||||
if (formats_used.includes(file_json['formats'][i]['format_id'])) {
|
||||
calculated_filesize += file_json['formats'][i]['filesize'];
|
||||
}
|
||||
Any file that starts with <video title> will be counted as part of the "bytes downloaded", which will
|
||||
be divided by the "total expected bytes."
|
||||
*/
|
||||
const file_id = download['file_id'];
|
||||
const filename = path.format(path.parse(download['_filename'].substring(0, download['_filename'].length-4)));
|
||||
const resulting_file_size = download['filesize'];
|
||||
|
||||
glob(`${filename}*`, (err, files) => {
|
||||
let sum_size = 0;
|
||||
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 {
|
||||
console.log('could not find json file');
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
});
|
||||
download['percent_complete'] = (sum_size/resulting_file_size * 100).toFixed(2);
|
||||
updateDownloads();
|
||||
});
|
||||
}
|
||||
|
||||
// youtube-dl functions
|
||||
@@ -1821,7 +1853,7 @@ app.post('/api/tomp3', optionalJwt, async function(req, res) {
|
||||
const is_playlist = url.includes('playlist');
|
||||
|
||||
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);
|
||||
else
|
||||
result_obj = await downloadFileByURL_normal(url, 'audio', options, req.query.sessionID);
|
||||
@@ -1833,6 +1865,7 @@ app.post('/api/tomp3', 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 options = {
|
||||
customArgs: req.body.customArgs,
|
||||
@@ -1850,7 +1883,7 @@ app.post('/api/tomp4', optionalJwt, async function(req, res) {
|
||||
const is_playlist = url.includes('playlist');
|
||||
|
||||
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);
|
||||
else
|
||||
result_obj = await downloadFileByURL_normal(url, 'video', options, req.query.sessionID);
|
||||
@@ -1878,6 +1911,14 @@ app.get('/api/getMp3s', optionalJwt, function(req, res) {
|
||||
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({
|
||||
mp3s: mp3s,
|
||||
playlists: playlists
|
||||
@@ -1897,6 +1938,14 @@ app.get('/api/getMp4s', optionalJwt, function(req, res) {
|
||||
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({
|
||||
mp4s: mp4s,
|
||||
playlists: playlists
|
||||
@@ -1941,7 +1990,7 @@ app.post('/api/getFile', optionalJwt, function (req, res) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/getAllFiles', optionalJwt, function (req, res) {
|
||||
app.post('/api/getAllFiles', optionalJwt, async function (req, res) {
|
||||
// these are returned
|
||||
let files = [];
|
||||
let playlists = [];
|
||||
@@ -1951,7 +2000,7 @@ app.post('/api/getAllFiles', optionalJwt, function (req, res) {
|
||||
let audios = null;
|
||||
let audio_playlists = null;
|
||||
let video_playlists = null;
|
||||
let subscriptions = subscriptions_api.getAllSubscriptions(req.isAuthenticated() ? req.user.uid : null);
|
||||
let subscriptions = config_api.getConfigItem('ytdl_allow_subscriptions') ? (subscriptions_api.getAllSubscriptions(req.isAuthenticated() ? req.user.uid : null)) : [];
|
||||
|
||||
// get basic info depending on multi-user mode being enabled
|
||||
if (req.isAuthenticated()) {
|
||||
@@ -1981,6 +2030,14 @@ app.post('/api/getAllFiles', optionalJwt, function (req, res) {
|
||||
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({
|
||||
files: files,
|
||||
playlists: playlists
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
"path-video": "video/",
|
||||
"use_youtubedl_archive": false,
|
||||
"custom_args": "",
|
||||
"safe_download_override": false
|
||||
"safe_download_override": false,
|
||||
"include_thumbnail": true,
|
||||
"include_metadata": true
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "YoutubeDL-Material",
|
||||
|
||||
@@ -397,7 +397,7 @@ exports.deleteUserFile = function(user_uid, file_uid, type, blacklistMode = fals
|
||||
|
||||
// use subscriptions API to remove video from the archive file, and write it to the blacklist
|
||||
if (fs.existsSync(archive_path)) {
|
||||
const line = youtube_id ? subscriptions_api.removeIDFromArchive(archive_path, youtube_id) : null;
|
||||
let line = youtube_id ? subscriptions_api.removeIDFromArchive(archive_path, youtube_id) : null;
|
||||
if (blacklistMode && line) {
|
||||
let blacklistPath = path.join(usersFileFolder, user_uid, 'archives', `blacklist_${type}.txt`);
|
||||
// adds newline to the beginning of the line
|
||||
|
||||
@@ -186,7 +186,9 @@ DEFAULT_CONFIG = {
|
||||
"path-video": "video/",
|
||||
"use_youtubedl_archive": false,
|
||||
"custom_args": "",
|
||||
"safe_download_override": false
|
||||
"safe_download_override": false,
|
||||
"include_thumbnail": true,
|
||||
"include_metadata": true
|
||||
},
|
||||
"Extra": {
|
||||
"title_top": "YoutubeDL-Material",
|
||||
|
||||
@@ -30,6 +30,14 @@ let CONFIG_ITEMS = {
|
||||
'key': 'ytdl_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
|
||||
'ytdl_title_top': {
|
||||
|
||||
@@ -26,11 +26,8 @@ function registerFileDB(file_path, type, multiUserMode = null, sub = null) {
|
||||
|
||||
utils.fixVideoMetadataPerms(file_id, type, multiUserMode && multiUserMode.file_path);
|
||||
|
||||
// add additional info
|
||||
file_object['uid'] = uuid();
|
||||
file_object['registered'] = Date.now();
|
||||
path_object = path.parse(file_object['path']);
|
||||
file_object['path'] = path.format(path_object);
|
||||
// add thumbnail path
|
||||
file_object['thumbnailPath'] = utils.getDownloadedThumbnail(file_id, type, multiUserMode && multiUserMode.file_path);
|
||||
|
||||
if (!sub) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -165,6 +168,10 @@ async function importUnregisteredFiles() {
|
||||
// add subscriptions to check list
|
||||
for (let i = 0; i < subscriptions_to_check.length; 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({
|
||||
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),
|
||||
|
||||
6
backend/package-lock.json
generated
6
backend/package-lock.json
generated
@@ -1968,9 +1968,9 @@
|
||||
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
|
||||
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
||||
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
|
||||
},
|
||||
"node-id3": {
|
||||
"version": "0.1.16",
|
||||
|
||||
@@ -37,12 +37,13 @@
|
||||
"express": "^4.17.1",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"fs-extra": "^9.0.0",
|
||||
"glob": "^7.1.6",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lowdb": "^1.0.0",
|
||||
"md5": "^2.2.1",
|
||||
"merge-files": "^0.1.2",
|
||||
"multer": "^1.4.2",
|
||||
"node-fetch": "^2.6.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-id3": "^0.1.14",
|
||||
"nodemon": "^2.0.2",
|
||||
"passport": "^0.4.1",
|
||||
|
||||
@@ -345,6 +345,10 @@ async function getVideosForSub(sub, user_uid = null) {
|
||||
}
|
||||
}
|
||||
|
||||
if (config_api.getConfigItem('ytdl_include_thumbnail')) {
|
||||
downloadConfig.push('--write-thumbnail');
|
||||
}
|
||||
|
||||
// get videos
|
||||
logger.verbose('Subscription: getting videos for subscription ' + sub.name);
|
||||
youtubedl.exec(sub.url, downloadConfig, {}, function(err, output) {
|
||||
|
||||
@@ -28,7 +28,7 @@ function getDownloadedFilesByType(basePath, type) {
|
||||
var located_files = recFindByExt(basePath, ext);
|
||||
for (let i = 0; i < located_files.length; 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);
|
||||
|
||||
@@ -88,6 +88,41 @@ function getJSONByType(type, name, customPath, openReadPerms = false) {
|
||||
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(info_json) {
|
||||
if (info_json['filesize']) {
|
||||
return info_json['filesize'];
|
||||
}
|
||||
|
||||
const formats = info_json['format_id'].split('+');
|
||||
let expected_filesize = 0;
|
||||
formats.forEach(format_id => {
|
||||
info_json.formats.forEach(available_format => {
|
||||
if (available_format.format_id === format_id && available_format.filesize) {
|
||||
expected_filesize += available_format.filesize;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return expected_filesize;
|
||||
}
|
||||
|
||||
function fixVideoMetadataPerms(name, type, customPath = null) {
|
||||
if (is_windows) return;
|
||||
if (!customPath) customPath = type === 'audio' ? config_api.getConfigItem('ytdl_audio_folder_path')
|
||||
@@ -110,6 +145,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)
|
||||
{
|
||||
files = files || fs.readdirSync(base)
|
||||
@@ -153,7 +201,10 @@ module.exports = {
|
||||
getJSONMp3: getJSONMp3,
|
||||
getJSONMp4: getJSONMp4,
|
||||
getTrueFileName: getTrueFileName,
|
||||
getDownloadedThumbnail: getDownloadedThumbnail,
|
||||
getExpectedFileSize: getExpectedFileSize,
|
||||
fixVideoMetadataPerms: fixVideoMetadataPerms,
|
||||
deleteJSONFile: deleteJSONFile,
|
||||
getDownloadedFilesByType: getDownloadedFilesByType,
|
||||
recFindByExt: recFindByExt,
|
||||
File: 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 .
|
||||
@@ -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
|
||||
@@ -38,15 +38,15 @@
|
||||
</div>
|
||||
<div class="sidenav-container" style="height: calc(100% - 64px)">
|
||||
<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>
|
||||
<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 && 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 && 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 && 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)="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')))">
|
||||
<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>
|
||||
</mat-nav-list>
|
||||
</mat-sidenav>
|
||||
|
||||
@@ -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 {FileCardComponent} from './file-card/file-card.component';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
@@ -30,11 +30,13 @@ import { SetDefaultAdminDialogComponent } from './dialogs/set-default-admin-dial
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
export class AppComponent implements OnInit, AfterViewInit {
|
||||
|
||||
@HostBinding('class') componentCssClass;
|
||||
THEMES_CONFIG = THEMES_CONFIG;
|
||||
|
||||
window = window;
|
||||
|
||||
// config items
|
||||
topBarTitle = 'Youtube Downloader';
|
||||
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() {
|
||||
this.sidenav.toggle();
|
||||
}
|
||||
@@ -89,9 +114,7 @@ export class AppComponent implements OnInit {
|
||||
|
||||
// gets the subscriptions
|
||||
if (this.allowSubscriptions) {
|
||||
this.postsService.getAllSubscriptions().subscribe(res => {
|
||||
this.postsService.subscriptions = res['subscriptions'];
|
||||
})
|
||||
this.postsService.reloadSubscriptions();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,9 +147,9 @@ export class AppComponent implements OnInit {
|
||||
this.postsService.setTheme(theme);
|
||||
|
||||
this.onSetTheme(this.THEMES_CONFIG[theme]['css_label'], old_theme ? this.THEMES_CONFIG[old_theme]['css_label'] : old_theme);
|
||||
}
|
||||
}
|
||||
|
||||
onSetTheme(theme, old_theme) {
|
||||
onSetTheme(theme, old_theme) {
|
||||
if (old_theme) {
|
||||
document.body.classList.remove(old_theme);
|
||||
this.overlayContainer.getContainerElement().classList.remove(old_theme);
|
||||
@@ -148,27 +171,6 @@ onSetTheme(theme, old_theme) {
|
||||
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() {
|
||||
|
||||
}
|
||||
|
||||
@@ -50,7 +50,8 @@ export class CustomPlaylistsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
goToPlaylist(playlist) {
|
||||
goToPlaylist(info_obj) {
|
||||
const playlist = info_obj.file;
|
||||
const playlistID = playlist.id;
|
||||
const type = playlist.type;
|
||||
|
||||
|
||||
@@ -31,12 +31,12 @@
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<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>
|
||||
</div>
|
||||
</ng-container>
|
||||
<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>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
@@ -55,17 +55,21 @@ export class RecentVideosComponent implements OnInit {
|
||||
if (localStorage.getItem('cached_file_count')) {
|
||||
this.cached_file_count = +localStorage.getItem('cached_file_count');
|
||||
this.loading_files = Array(this.cached_file_count).fill(0);
|
||||
console.log(this.loading_files);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.postsService.initialized) {
|
||||
this.getAllFiles();
|
||||
}
|
||||
|
||||
this.postsService.service_initialized.subscribe(init => {
|
||||
if (init) {
|
||||
this.getAllFiles();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// set filter property to cached
|
||||
const cached_filter_property = localStorage.getItem('filter_property');
|
||||
if (cached_filter_property && this.filterProperties[cached_filter_property]) {
|
||||
@@ -134,28 +138,36 @@ export class RecentVideosComponent implements OnInit {
|
||||
|
||||
// navigation
|
||||
|
||||
goToFile(file) {
|
||||
goToFile(info_obj) {
|
||||
const file = info_obj['file'];
|
||||
const event = info_obj['event'];
|
||||
if (this.postsService.config['Extra']['download_only_mode']) {
|
||||
this.downloadFile(file);
|
||||
} else {
|
||||
this.navigateToFile(file);
|
||||
this.navigateToFile(file, event.ctrlKey);
|
||||
}
|
||||
}
|
||||
|
||||
navigateToFile(file) {
|
||||
navigateToFile(file, new_tab) {
|
||||
localStorage.setItem('player_navigator', this.router.url);
|
||||
if (file.sub_id) {
|
||||
const sub = this.postsService.getSubscriptionByID(file.sub_id)
|
||||
const sub = this.postsService.getSubscriptionByID(file.sub_id);
|
||||
if (sub.streamingOnly) {
|
||||
this.router.navigate(['/player', {name: file.id,
|
||||
url: file.requested_formats ? file.requested_formats[0].url : file.url}]);
|
||||
// streaming only mode subscriptions
|
||||
!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 {
|
||||
this.router.navigate(['/player', {fileNames: file.id,
|
||||
type: file.isAudio ? 'audio' : 'video', subscriptionName: sub.name,
|
||||
subPlaylist: sub.isPlaylist}]);
|
||||
// normal subscriptions
|
||||
!new_tab ? this.router.navigate(['/player', {fileNames: file.id,
|
||||
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 {
|
||||
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 ext = type === 'audio' ? '.mp3' : '.mp4'
|
||||
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.user ? this.postsService.user.uid : null, null).subscribe(res => {
|
||||
const blob: Blob = res;
|
||||
|
||||
@@ -25,11 +25,11 @@
|
||||
<button mat-menu-item>Placeholder</button>
|
||||
</ng-container>
|
||||
</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 *ngIf="!loading && file_obj.thumbnailURL" class="img-div">
|
||||
<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">
|
||||
{{file_length}}
|
||||
</div>
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
background: rgba(255,255,255,0.6);
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.download-time {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { VideoInfoDialogComponent } from 'app/dialogs/video-info-dialog/video-info-dialog.component';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
|
||||
@Component({
|
||||
selector: 'app-unified-file-card',
|
||||
@@ -16,6 +17,10 @@ export class UnifiedFileCardComponent implements OnInit {
|
||||
type = null;
|
||||
elevated = false;
|
||||
|
||||
// optional vars
|
||||
thumbnailBlobURL = null;
|
||||
|
||||
// input/output
|
||||
@Input() loading = true;
|
||||
@Input() theme = null;
|
||||
@Input() file_obj = null;
|
||||
@@ -35,12 +40,19 @@ export class UnifiedFileCardComponent implements OnInit {
|
||||
big: 250x200
|
||||
*/
|
||||
|
||||
constructor(private dialog: MatDialog) { }
|
||||
constructor(private dialog: MatDialog, private sanitizer: DomSanitizer) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.loading) {
|
||||
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) {
|
||||
@@ -51,8 +63,8 @@ export class UnifiedFileCardComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
navigateToFile() {
|
||||
this.goToFile.emit(this.file_obj);
|
||||
navigateToFile(event) {
|
||||
this.goToFile.emit({file: this.file_obj, event: event});
|
||||
}
|
||||
|
||||
navigateToSubscription() {
|
||||
@@ -97,3 +109,16 @@ function fancyTimeFormat(time) {
|
||||
ret += '' + secs;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -164,7 +164,7 @@
|
||||
<br/>
|
||||
<div class="centered big" id="bar_div" *ngIf="current_download && current_download.downloading; else nofile">
|
||||
<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>
|
||||
<br/>
|
||||
</div>
|
||||
@@ -181,10 +181,12 @@
|
||||
|
||||
</ng-template>
|
||||
|
||||
<app-recent-videos></app-recent-videos>
|
||||
<br/>
|
||||
<h4 style="text-align: center">Custom playlists</h4>
|
||||
<app-custom-playlists></app-custom-playlists>
|
||||
<ng-container *ngIf="cachedFileManagerEnabled || fileManagerEnabled">
|
||||
<app-recent-videos></app-recent-videos>
|
||||
<br/>
|
||||
<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'))">
|
||||
<mat-accordion>
|
||||
|
||||
@@ -82,8 +82,9 @@ export class MainComponent implements OnInit {
|
||||
useDefaultDownloadingAgent = true;
|
||||
customDownloadingAgent = null;
|
||||
|
||||
// formats cache
|
||||
// cache
|
||||
cachedAvailableFormats = {};
|
||||
cachedFileManagerEnabled = localStorage.getItem('cached_filemanager_enabled') === 'true';
|
||||
|
||||
// youtube api
|
||||
youtubeSearchEnabled = false;
|
||||
@@ -232,7 +233,8 @@ export class MainComponent implements OnInit {
|
||||
|
||||
async loadConfig() {
|
||||
// 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.allowMultiDownloadMode = this.postsService.config['Extra']['allow_multi_download_mode'];
|
||||
this.audioFolderPath = this.postsService.config['Downloader']['path-audio'];
|
||||
@@ -261,6 +263,10 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
|
||||
// set final cache items
|
||||
|
||||
localStorage.setItem('cached_filemanager_enabled', this.fileManagerEnabled.toString());
|
||||
this.cachedFileManagerEnabled = this.fileManagerEnabled;
|
||||
|
||||
if (this.allowAdvancedDownload) {
|
||||
if (localStorage.getItem('customArgsEnabled') !== null) {
|
||||
this.customArgsEnabled = localStorage.getItem('customArgsEnabled') === 'true';
|
||||
@@ -1033,8 +1039,8 @@ export class MainComponent implements OnInit {
|
||||
}
|
||||
} else if (format_obj.type === 'video') {
|
||||
// check if video format is mp4
|
||||
const key = format.height.toString();
|
||||
if (format.ext === 'mp4') {
|
||||
const key = format.format_note.replace('p', '');
|
||||
if (format.ext === 'mp4' || format.ext === 'mkv' || format.ext === 'webm') {
|
||||
format_obj['height'] = format.height;
|
||||
format_obj['acodec'] = format.acodec;
|
||||
format_obj['format_id'] = format.format_id;
|
||||
|
||||
@@ -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 { PostsService } from 'app/posts.services';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
@@ -20,7 +20,7 @@ export interface IMedia {
|
||||
templateUrl: './player.component.html',
|
||||
styleUrls: ['./player.component.css']
|
||||
})
|
||||
export class PlayerComponent implements OnInit, OnDestroy {
|
||||
export class PlayerComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
playlist: Array<IMedia> = [];
|
||||
original_playlist: string = null;
|
||||
@@ -95,6 +95,10 @@ export class PlayerComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.postsService.sidenav.close();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// prevents volume save feature from running in the background
|
||||
clearInterval(this.save_volume_timer);
|
||||
|
||||
@@ -15,16 +15,14 @@ import * as Fingerprint2 from 'fingerprintjs2';
|
||||
@Injectable()
|
||||
export class PostsService implements CanActivate {
|
||||
path = '';
|
||||
audioFolder = '';
|
||||
videoFolder = '';
|
||||
startPath = null; // 'http://localhost:17442/';
|
||||
startPathSSL = null; // 'https://localhost:17442/'
|
||||
handShakeComplete = false;
|
||||
|
||||
// local settings
|
||||
THEMES_CONFIG = THEMES_CONFIG;
|
||||
theme;
|
||||
card_size = 'medium';
|
||||
sidepanel_mode = 'over';
|
||||
settings_changed = new BehaviorSubject<boolean>(false);
|
||||
|
||||
// auth
|
||||
auth_token = '4241b401-7236-493e-92b5-b72696b9d853';
|
||||
session_id = null;
|
||||
httpOptions = null;
|
||||
@@ -41,20 +39,24 @@ export class PostsService implements CanActivate {
|
||||
|
||||
available_permissions = null;
|
||||
|
||||
// behavior subjects
|
||||
reload_config = new BehaviorSubject<boolean>(false);
|
||||
config_reloaded = 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);
|
||||
|
||||
// app status
|
||||
initialized = false;
|
||||
|
||||
// global vars
|
||||
config = null;
|
||||
subscriptions = null;
|
||||
sidenav = null;
|
||||
|
||||
constructor(private http: HttpClient, private router: Router, @Inject(DOCUMENT) private document: Document,
|
||||
public snackBar: MatSnackBar) {
|
||||
console.log('PostsService Initialized...');
|
||||
// this.startPath = window.location.href + '/api/';
|
||||
// this.startPathSSL = window.location.href + '/api/';
|
||||
this.path = this.document.location.origin + '/api/';
|
||||
|
||||
if (isDevMode()) {
|
||||
@@ -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
|
||||
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,
|
||||
@@ -398,6 +392,8 @@ export class PostsService implements CanActivate {
|
||||
}, err => {
|
||||
if (err.status === 401) {
|
||||
this.sendToLogin();
|
||||
this.token = null;
|
||||
this.resetHttpParams();
|
||||
}
|
||||
console.log(err);
|
||||
});
|
||||
@@ -414,14 +410,7 @@ export class PostsService implements CanActivate {
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
|
||||
// resets http params
|
||||
this.http_params = `apiKey=${this.auth_token}&sessionID=${this.session_id}`
|
||||
|
||||
this.httpOptions = {
|
||||
params: new HttpParams({
|
||||
fromString: this.http_params
|
||||
}),
|
||||
};
|
||||
this.resetHttpParams();
|
||||
}
|
||||
|
||||
// user methods
|
||||
@@ -446,12 +435,29 @@ export class PostsService implements CanActivate {
|
||||
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() {
|
||||
this.service_initialized.next(true);
|
||||
this.initialized = true;
|
||||
this.config_reloaded.next(true);
|
||||
}
|
||||
|
||||
reloadSubscriptions() {
|
||||
this.getAllSubscriptions().subscribe(res => {
|
||||
this.subscriptions = res['subscriptions'];
|
||||
});
|
||||
}
|
||||
|
||||
adminExists() {
|
||||
return this.http.post(this.path + 'auth/adminExists', {}, this.httpOptions);
|
||||
}
|
||||
|
||||
@@ -128,7 +128,11 @@
|
||||
</div>
|
||||
|
||||
<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 class="col-12 mt-2">
|
||||
|
||||
@@ -80,6 +80,7 @@ export class SubscriptionsComponent implements OnInit {
|
||||
} else {
|
||||
this.channel_subscriptions.push(result);
|
||||
}
|
||||
this.postsService.reloadSubscriptions();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -96,6 +97,7 @@ export class SubscriptionsComponent implements OnInit {
|
||||
if (success) {
|
||||
this.openSnackBar(`${sub.name} successfully deleted!`)
|
||||
this.getSubscriptions();
|
||||
this.postsService.reloadSubscriptions();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user