Add interactive cookie test dialog and API

pull/1163/head
voc0der 2 months ago
parent 1be8e22772
commit d1a5e762ed

@ -156,7 +156,7 @@ If you're interested in translating the app into a new language, check out the [
Official translators:
* Spanish - voc0der
* Spanish - tzahi12345
* German - UnlimitedCookies
* Chinese - TyRoyal

@ -1595,6 +1595,202 @@ app.post('/api/uploadCookies', upload_multer.single('cookies'), async (req, res)
});
function getCookiesFileSummary(cookies_text) {
const lines = cookies_text.split(/\r?\n/).map(line => line.trim()).filter(line => line.length > 0);
const cookie_lines = lines.filter(line => !line.startsWith('#') || line.startsWith('#HttpOnly_'));
let invalid_entries = 0;
for (const line of cookie_lines) {
// Netscape cookie format should contain at least 7 tab-separated values.
if (line.split('\t').length < 7) invalid_entries++;
}
return {
total_lines: lines.length,
cookie_entries: cookie_lines.length,
invalid_entries: invalid_entries
};
}
function normalizeCookieTestError(err) {
if (!err) return 'Unknown error.';
let message = null;
if (typeof err === 'string') {
message = err;
} else if (err.stderr) {
message = err.stderr.toString();
} else if (err.message) {
message = err.message.toString();
} else {
message = JSON.stringify(err);
}
if (!message) return 'Unknown error.';
const max_error_length = 1200;
return message.length > max_error_length ? message.substring(0, max_error_length) + '...' : message;
}
app.post('/api/testCookies', optionalJwt, async (req, res) => {
const logs = [];
const use_cookies_enabled = config_api.getConfigItem('ytdl_use_cookies');
const downloader = config_api.getConfigItem('ytdl_default_downloader');
const test_url = req.body && req.body.url ? req.body.url.trim() : '';
const cookie_path = path.join(__dirname, 'appdata', 'cookies.txt');
const relative_cookie_path = path.join('appdata', 'cookies.txt');
logs.push('Starting cookie test.');
logs.push(`Downloader: ${downloader}`);
logs.push(`Use Cookies setting is ${use_cookies_enabled ? 'enabled' : 'disabled'}.`);
if (!test_url) {
logs.push('No URL was provided for cookie testing.');
res.status(400).send({
success: false,
error: 'Missing URL to test.',
logs: logs,
use_cookies_enabled: use_cookies_enabled,
cookie_file_found: false
});
return;
}
let parsed_test_url = null;
try {
parsed_test_url = new URL(test_url);
} catch (err) {
parsed_test_url = null;
}
if (!parsed_test_url || (parsed_test_url.protocol !== 'http:' && parsed_test_url.protocol !== 'https:')) {
logs.push(`Invalid test URL provided: ${test_url}`);
res.status(400).send({
success: false,
error: 'Invalid URL. Only http/https URLs are allowed.',
logs: logs,
use_cookies_enabled: use_cookies_enabled,
cookie_file_found: false
});
return;
}
if (!(await fs.pathExists(cookie_path))) {
logs.push(`Cookie file was not found at ${cookie_path}.`);
res.send({
success: false,
error: 'Cookies file not found.',
logs: logs,
use_cookies_enabled: use_cookies_enabled,
cookie_file_found: false
});
return;
}
const cookie_stats = await fs.stat(cookie_path);
logs.push(`Cookie file found (${cookie_stats.size} bytes).`);
if (cookie_stats.size === 0) {
logs.push('Cookie file is empty.');
res.send({
success: false,
error: 'Cookies file is empty.',
logs: logs,
use_cookies_enabled: use_cookies_enabled,
cookie_file_found: true,
cookie_file_size: cookie_stats.size
});
return;
}
const cookies_text = await fs.readFile(cookie_path, 'utf8');
const cookie_summary = getCookiesFileSummary(cookies_text);
logs.push(`Detected ${cookie_summary.cookie_entries} cookie entries from ${cookie_summary.total_lines} non-empty lines.`);
if (cookie_summary.invalid_entries > 0) {
logs.push(`Detected ${cookie_summary.invalid_entries} entries that may not be valid Netscape cookie rows.`);
}
const args = [
'--skip-download',
'--no-warnings',
'--no-playlist',
'--dump-single-json',
'--cookies',
relative_cookie_path
];
logs.push(`Testing URL: ${test_url}`);
logs.push(`Executing test command with cookies at ${relative_cookie_path}.`);
let run_response = null;
try {
run_response = await youtubedl_api.runYoutubeDL(test_url, args);
} catch (err) {
const error_message = normalizeCookieTestError(err);
logs.push(`Failed to start downloader process. ${error_message}`);
res.status(500).send({
success: false,
error: error_message,
logs: logs,
use_cookies_enabled: use_cookies_enabled,
cookie_file_found: true,
cookie_summary: cookie_summary
});
return;
}
if (!run_response || !run_response.callback) {
logs.push('Downloader process did not initialize correctly.');
res.status(500).send({
success: false,
error: 'Failed to initialize downloader process.',
logs: logs,
use_cookies_enabled: use_cookies_enabled,
cookie_file_found: true,
cookie_summary: cookie_summary
});
return;
}
const {parsed_output, err} = await run_response.callback;
if (parsed_output && parsed_output.length > 0) {
const info_obj = parsed_output[0];
const title = info_obj && info_obj.title ? info_obj.title : null;
const extractor = info_obj && info_obj.extractor ? info_obj.extractor : null;
if (title) logs.push(`Metadata fetch succeeded: "${title}".`);
else logs.push('Metadata fetch succeeded.');
if (extractor) logs.push(`Extractor used: ${extractor}.`);
res.send({
success: true,
logs: logs,
use_cookies_enabled: use_cookies_enabled,
cookie_file_found: true,
cookie_file_size: cookie_stats.size,
cookie_summary: cookie_summary,
result: {
title: title,
extractor: extractor
}
});
return;
}
const error_message = normalizeCookieTestError(err);
logs.push('Metadata fetch failed while using cookies.');
logs.push(error_message);
res.send({
success: false,
error: error_message,
logs: logs,
use_cookies_enabled: use_cookies_enabled,
cookie_file_found: true,
cookie_file_size: cookie_stats.size,
cookie_summary: cookie_summary
});
});
// Updater API calls
app.get('/api/updaterStatus', optionalJwt, async (req, res) => {

@ -33,8 +33,31 @@
</tbody>
</table>
</div>
<mat-divider></mat-divider>
<div class="cookie-test-section">
<h5 i18n="Cookies test section title">Test cookies</h5>
<p i18n="Cookies test section description">Run a metadata-only check with your uploaded cookies and inspect logs to verify the setup.</p>
<mat-form-field class="cookie-test-url">
<mat-label i18n="Cookies test URL label">Test URL</mat-label>
<input matInput [(ngModel)]="cookiesTestUrl" placeholder="https://www.youtube.com/watch?v=..." i18n-placeholder="Cookies test URL placeholder">
</mat-form-field>
<div class="cookie-test-actions">
<button mat-flat-button color="accent" (click)="runCookiesTest()" [disabled]="testingCookies || !cookiesTestUrl || !cookiesTestUrl.trim()">
<ng-container i18n="Run cookies test button">Run Cookie Test</ng-container>
</button>
<mat-spinner *ngIf="testingCookies" class="cookie-test-spinner" [diameter]="22"></mat-spinner>
</div>
<div class="cookie-test-status" *ngIf="cookiesTestComplete && cookiesTestSuccess !== null">
<span *ngIf="cookiesTestSuccess" class="cookie-test-success" i18n="Cookies test success message">Cookies test passed.</span>
<span *ngIf="cookiesTestSuccess === false" class="cookie-test-failed" i18n="Cookies test failure message">Cookies test failed.</span>
</div>
<div class="cookie-test-logs-wrapper" *ngIf="cookiesTestLogs.length > 0">
<div class="cookie-test-logs-label" i18n="Cookies test logs title">Test logs</div>
<pre class="cookie-test-logs">{{ cookiesTestLogs.join('\n') }}</pre>
</div>
</div>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions><button style="margin-bottom: 5px;" mat-dialog-close mat-stroked-button><ng-container i18n="Close">Close</ng-container></button></mat-dialog-actions>
<mat-dialog-actions><button style="margin-bottom: 5px;" mat-dialog-close mat-stroked-button><ng-container i18n="Close">Close</ng-container></button></mat-dialog-actions>

@ -2,4 +2,61 @@
bottom: 1px;
left: 0.5px;
position: absolute;
}
}
.cookie-test-section {
margin-top: 16px;
text-align: left;
}
.cookie-test-url {
width: 100%;
}
.cookie-test-actions {
align-items: center;
display: flex;
gap: 10px;
margin-top: 2px;
}
.cookie-test-spinner {
margin-top: 1px;
}
.cookie-test-status {
margin-top: 10px;
}
.cookie-test-success {
color: #6cc070;
font-weight: 500;
}
.cookie-test-failed {
color: #f47070;
font-weight: 500;
}
.cookie-test-logs-wrapper {
margin-top: 12px;
}
.cookie-test-logs-label {
font-size: 13px;
margin-bottom: 6px;
opacity: 0.9;
}
.cookie-test-logs {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 6px;
font-size: 12px;
line-height: 1.5;
margin: 0;
max-height: 220px;
overflow: auto;
padding: 10px;
white-space: pre-wrap;
}

@ -2,6 +2,11 @@ import { Component, OnInit } from '@angular/core';
import { NgxFileDropEntry, FileSystemFileEntry, FileSystemDirectoryEntry } from 'ngx-file-drop';
import { PostsService } from 'app/posts.services';
type CookiesTestResponse = {
success: boolean;
logs: string[];
};
@Component({
selector: 'app-cookies-uploader-dialog',
templateUrl: './cookies-uploader-dialog.component.html',
@ -12,6 +17,11 @@ export class CookiesUploaderDialogComponent implements OnInit {
uploading = false;
uploaded = false;
testingCookies = false;
cookiesTestComplete = false;
cookiesTestSuccess: boolean = null;
cookiesTestUrl = '';
cookiesTestLogs: string[] = [];
constructor(private postsService: PostsService) { }
@ -47,6 +57,40 @@ export class CookiesUploaderDialogComponent implements OnInit {
}
}
runCookiesTest(): void {
const testUrl = this.cookiesTestUrl ? this.cookiesTestUrl.trim() : '';
if (!testUrl) {
this.postsService.openSnackBar($localize`Please provide a URL to test.`);
return;
}
this.testingCookies = true;
this.cookiesTestComplete = false;
this.cookiesTestSuccess = null;
this.cookiesTestLogs = [$localize`Running cookies test...`];
this.postsService.testCookies(testUrl).subscribe((res: CookiesTestResponse) => {
this.testingCookies = false;
this.cookiesTestComplete = true;
this.cookiesTestSuccess = !!res['success'];
this.cookiesTestLogs = Array.isArray(res['logs']) ? res['logs'] : [];
if (this.cookiesTestSuccess) {
this.postsService.openSnackBar($localize`Cookies test passed.`);
} else {
this.postsService.openSnackBar($localize`Cookies test failed. Review the popup logs.`);
}
}, err => {
this.testingCookies = false;
this.cookiesTestComplete = true;
this.cookiesTestSuccess = false;
const error = err && err.error ? err.error : null;
const errorLogs = error && Array.isArray(error['logs']) ? error['logs'] : null;
this.cookiesTestLogs = errorLogs && errorLogs.length > 0 ? errorLogs : [$localize`Cookies test failed due to a server error.`];
this.postsService.openSnackBar($localize`Cookies test failed. Review the popup logs.`);
});
}
public fileOver(event) {
}

@ -440,6 +440,10 @@ export class PostsService implements CanActivate {
return this.http.post<SuccessObject>(this.path + 'uploadCookies', formData, this.httpOptions);
}
testCookies(url: string) {
return this.http.post(this.path + 'testCookies', {url: url}, this.httpOptions);
}
downloadArchive(type: FileType, sub_id: string) {
const body: DownloadArchiveRequest = {type: type, sub_id: sub_id};
return this.http.post(this.path + 'downloadArchive', body, {responseType: 'blob', params: this.httpOptions.params});

Loading…
Cancel
Save