mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 12:06:16 -08:00
feat(settings): secure playlist backup credentials
This commit is contained in:
1 parent
04e9027840
commit
6726863081
27 files changed
+874
-4
No files matched your search
@@ -0,0 +1,105 @@
|
||||
<h2 mat-dialog-title>
|
||||
{{ 'SETTINGS.BACKUP_CREDENTIALS_DIALOG.TITLE' | translate }}
|
||||
</h2>
|
||||
|
||||
<form
|
||||
class="settings-backup-credentials-dialog"
|
||||
[formGroup]="credentialsForm"
|
||||
(ngSubmit)="submit()"
|
||||
novalidate
|
||||
>
|
||||
<mat-dialog-content>
|
||||
<p class="settings-backup-credentials-dialog__lead">
|
||||
{{ 'SETTINGS.BACKUP_CREDENTIALS_DIALOG.MESSAGE' | translate }}
|
||||
</p>
|
||||
|
||||
<dl class="settings-backup-credentials-dialog__context">
|
||||
<div>
|
||||
<dt>
|
||||
{{
|
||||
'SETTINGS.BACKUP_CREDENTIALS_DIALOG.PLAYLIST'
|
||||
| translate
|
||||
}}
|
||||
</dt>
|
||||
<dd>{{ dialogData.playlistTitle }}</dd>
|
||||
</div>
|
||||
@if (dialogData.serverHost) {
|
||||
<div>
|
||||
<dt>
|
||||
{{
|
||||
'SETTINGS.BACKUP_CREDENTIALS_DIALOG.SERVER'
|
||||
| translate
|
||||
}}
|
||||
</dt>
|
||||
<dd>{{ dialogData.serverHost }}</dd>
|
||||
</div>
|
||||
}
|
||||
</dl>
|
||||
|
||||
<div class="settings-backup-credentials-dialog__fields">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>
|
||||
{{
|
||||
'SETTINGS.BACKUP_CREDENTIALS_DIALOG.USERNAME'
|
||||
| translate
|
||||
}}
|
||||
</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="username"
|
||||
autocomplete="username"
|
||||
cdkFocusInitial
|
||||
/>
|
||||
@if (
|
||||
credentialsForm.controls.username.invalid &&
|
||||
credentialsForm.controls.username.touched
|
||||
) {
|
||||
<mat-error>
|
||||
{{
|
||||
'SETTINGS.BACKUP_CREDENTIALS_DIALOG.USERNAME_REQUIRED'
|
||||
| translate
|
||||
}}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>
|
||||
{{
|
||||
'SETTINGS.BACKUP_CREDENTIALS_DIALOG.PASSWORD'
|
||||
| translate
|
||||
}}
|
||||
</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="password"
|
||||
formControlName="password"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
@if (
|
||||
credentialsForm.controls.password.invalid &&
|
||||
credentialsForm.controls.password.touched
|
||||
) {
|
||||
<mat-error>
|
||||
{{
|
||||
'SETTINGS.BACKUP_CREDENTIALS_DIALOG.PASSWORD_REQUIRED'
|
||||
| translate
|
||||
}}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions
|
||||
align="end"
|
||||
class="settings-backup-credentials-dialog__actions"
|
||||
>
|
||||
<button mat-button type="button" (click)="skip()">
|
||||
{{ 'SETTINGS.BACKUP_CREDENTIALS_DIALOG.SKIP' | translate }}
|
||||
</button>
|
||||
<button mat-flat-button type="submit">
|
||||
{{ 'SETTINGS.BACKUP_CREDENTIALS_DIALOG.IMPORT' | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</form>
|
||||
@@ -0,0 +1,55 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.settings-backup-credentials-dialog {
|
||||
min-width: min(100%, 380px);
|
||||
}
|
||||
|
||||
.settings-backup-credentials-dialog__lead {
|
||||
margin: 0;
|
||||
line-height: 1.55;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.settings-backup-credentials-dialog__context {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin: 18px 0;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--mat-sys-outline) 42%, transparent);
|
||||
border-radius: 14px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--mat-sys-surface-container) 84%,
|
||||
transparent
|
||||
);
|
||||
|
||||
div {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(72px, auto) minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
dt {
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-backup-credentials-dialog__fields {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.settings-backup-credentials-dialog__actions {
|
||||
padding-top: 2px;
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
SettingsBackupCredentialsDialogComponent,
|
||||
SettingsBackupCredentialsDialogData,
|
||||
} from './settings-backup-credentials-dialog.component';
|
||||
|
||||
describe('SettingsBackupCredentialsDialogComponent', () => {
|
||||
let component: SettingsBackupCredentialsDialogComponent;
|
||||
let fixture: ComponentFixture<SettingsBackupCredentialsDialogComponent>;
|
||||
let dialogRef: { close: jest.Mock };
|
||||
|
||||
beforeEach(async () => {
|
||||
dialogRef = {
|
||||
close: jest.fn(),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
SettingsBackupCredentialsDialogComponent,
|
||||
NoopAnimationsModule,
|
||||
TranslateModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: MAT_DIALOG_DATA,
|
||||
useValue: {
|
||||
playlistTitle: 'Living room',
|
||||
serverHost: 'provider.example:8443',
|
||||
} satisfies SettingsBackupCredentialsDialogData,
|
||||
},
|
||||
{
|
||||
provide: MatDialogRef,
|
||||
useValue: dialogRef,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
const translate = TestBed.inject(TranslateService);
|
||||
translate.setTranslation(
|
||||
'en',
|
||||
{
|
||||
SETTINGS: {
|
||||
BACKUP_CREDENTIALS_DIALOG: {
|
||||
TITLE: 'Xtream credentials required',
|
||||
MESSAGE:
|
||||
'This redacted backup does not contain the credentials for this playlist.',
|
||||
PLAYLIST: 'Playlist',
|
||||
SERVER: 'Server',
|
||||
USERNAME: 'Username',
|
||||
PASSWORD: 'Password',
|
||||
USERNAME_REQUIRED: 'Enter a username.',
|
||||
PASSWORD_REQUIRED: 'Enter a password.',
|
||||
SKIP: 'Skip playlist',
|
||||
IMPORT: 'Import playlist',
|
||||
},
|
||||
},
|
||||
},
|
||||
true
|
||||
);
|
||||
translate.use('en');
|
||||
|
||||
fixture = TestBed.createComponent(
|
||||
SettingsBackupCredentialsDialogComponent
|
||||
);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('identifies the redacted playlist without exposing a full source URL', () => {
|
||||
const text = (fixture.nativeElement as HTMLElement).textContent ?? '';
|
||||
|
||||
expect(text).toContain('Xtream credentials required');
|
||||
expect(text).toContain('Living room');
|
||||
expect(text).toContain('provider.example:8443');
|
||||
});
|
||||
|
||||
it('marks blank credentials as invalid and keeps the dialog open', () => {
|
||||
component.credentialsForm.setValue({
|
||||
username: ' ',
|
||||
password: ' ',
|
||||
});
|
||||
|
||||
component.submit();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.credentialsForm.invalid).toBe(true);
|
||||
expect(component.credentialsForm.controls.username.touched).toBe(true);
|
||||
expect(component.credentialsForm.controls.password.touched).toBe(true);
|
||||
expect(dialogRef.close).not.toHaveBeenCalled();
|
||||
|
||||
const text = (fixture.nativeElement as HTMLElement).textContent ?? '';
|
||||
expect(text).toContain('Enter a username.');
|
||||
expect(text).toContain('Enter a password.');
|
||||
});
|
||||
|
||||
it('returns a trimmed username while preserving the password exactly', () => {
|
||||
component.credentialsForm.setValue({
|
||||
username: ' viewer ',
|
||||
password: ' secret with spaces ',
|
||||
});
|
||||
|
||||
component.submit();
|
||||
|
||||
expect(dialogRef.close).toHaveBeenCalledWith({
|
||||
username: 'viewer',
|
||||
password: ' secret with spaces ',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null when the playlist is skipped', () => {
|
||||
component.skip();
|
||||
|
||||
expect(dialogRef.close).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import {
|
||||
NonNullableFormBuilder,
|
||||
ReactiveFormsModule,
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogModule,
|
||||
MatDialogRef,
|
||||
} from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { PlaylistBackupXtreamCredentials } from '@iptvnator/services';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
export interface SettingsBackupCredentialsDialogData {
|
||||
playlistTitle: string;
|
||||
serverHost: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings-backup-credentials-dialog',
|
||||
templateUrl: './settings-backup-credentials-dialog.component.html',
|
||||
styleUrls: ['./settings-backup-credentials-dialog.component.scss'],
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
ReactiveFormsModule,
|
||||
TranslateModule,
|
||||
],
|
||||
})
|
||||
export class SettingsBackupCredentialsDialogComponent {
|
||||
private readonly dialogRef =
|
||||
inject<
|
||||
MatDialogRef<
|
||||
SettingsBackupCredentialsDialogComponent,
|
||||
PlaylistBackupXtreamCredentials | null
|
||||
>
|
||||
>(MatDialogRef);
|
||||
private readonly formBuilder = inject(NonNullableFormBuilder);
|
||||
|
||||
readonly dialogData =
|
||||
inject<SettingsBackupCredentialsDialogData>(MAT_DIALOG_DATA);
|
||||
|
||||
readonly credentialsForm = this.formBuilder.group({
|
||||
username: ['', [Validators.required, Validators.pattern(/\S/)]],
|
||||
password: ['', [Validators.required, Validators.pattern(/\S/)]],
|
||||
});
|
||||
|
||||
submit(): void {
|
||||
if (this.credentialsForm.invalid) {
|
||||
this.credentialsForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
const { username, password } = this.credentialsForm.getRawValue();
|
||||
this.dialogRef.close({
|
||||
username: username.trim(),
|
||||
password,
|
||||
});
|
||||
}
|
||||
|
||||
skip(): void {
|
||||
this.dialogRef.close(null);
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,37 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-item__meta">
|
||||
<h4>{{ 'SETTINGS.BACKUP_CONTENTS' | translate }}</h4>
|
||||
<p id="backup-redacted-warning">
|
||||
{{ 'SETTINGS.BACKUP_REDACTED_WARNING' | translate }}
|
||||
</p>
|
||||
@if (includeSecrets()) {
|
||||
<p
|
||||
id="backup-secrets-warning"
|
||||
class="settings-backup-warning--secrets"
|
||||
>
|
||||
{{ 'SETTINGS.BACKUP_SECRETS_WARNING' | translate }}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
<div class="setting-item__control">
|
||||
<mat-checkbox
|
||||
data-test-id="backup-include-secrets"
|
||||
[checked]="includeSecrets()"
|
||||
[attr.aria-describedby]="
|
||||
includeSecrets()
|
||||
? 'backup-redacted-warning backup-secrets-warning'
|
||||
: 'backup-redacted-warning'
|
||||
"
|
||||
(change)="includeSecrets.set($event.checked)"
|
||||
>
|
||||
{{ 'SETTINGS.BACKUP_INCLUDE_SECRETS' | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (isPwa()) {
|
||||
<div class="settings-note">
|
||||
{{ 'SETTINGS.EPG_NOTE' | translate }}
|
||||
|
||||
@@ -1,26 +1,49 @@
|
||||
import { Component, input, output, ViewEncapsulation } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
inject,
|
||||
input,
|
||||
output,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { SettingsBackupFacade } from './settings-backup.facade';
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings-backup-section',
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
MatIconModule,
|
||||
MatProgressSpinnerModule,
|
||||
TranslateModule,
|
||||
],
|
||||
templateUrl: './settings-backup-section.component.html',
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
styles: [':host { display: contents; }'],
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.settings-backup-warning--secrets {
|
||||
color: var(--mat-sys-error);
|
||||
font-weight: 600;
|
||||
}
|
||||
`,
|
||||
],
|
||||
})
|
||||
export class SettingsBackupSectionComponent {
|
||||
private readonly backupFacade = inject(SettingsBackupFacade);
|
||||
|
||||
readonly activeSection = input.required<string>();
|
||||
readonly isPwa = input(false);
|
||||
readonly isRemovingAllPlaylists = input(false);
|
||||
readonly isExportingData = input(false);
|
||||
readonly includeSecrets = this.backupFacade.includeSecrets;
|
||||
|
||||
readonly importData = output<void>();
|
||||
readonly exportData = output<void>();
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { inject, Injectable, signal } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { PlaylistActions } from '@iptvnator/m3u-state';
|
||||
import {
|
||||
PlaylistBackupImportSummary,
|
||||
PlaylistBackupService,
|
||||
PlaylistBackupXtreamCredentials,
|
||||
PlaylistBackupXtreamCredentialsRequest,
|
||||
RuntimeCapabilitiesService,
|
||||
} from '@iptvnator/services';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import {
|
||||
SettingsBackupCredentialsDialogComponent,
|
||||
SettingsBackupCredentialsDialogData,
|
||||
} from './settings-backup-credentials-dialog.component';
|
||||
import { SettingsSnackbarService } from './settings-snackbar.service';
|
||||
|
||||
@Injectable()
|
||||
@@ -16,7 +24,9 @@ export class SettingsBackupFacade {
|
||||
private readonly settingsSnackbar = inject(SettingsSnackbarService);
|
||||
private readonly store = inject(Store);
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly matDialog = inject(MatDialog);
|
||||
|
||||
readonly includeSecrets = signal(false);
|
||||
readonly isExportingData = signal(false);
|
||||
|
||||
async exportData(waitForUiFeedbackFrame: () => Promise<void>) {
|
||||
@@ -28,7 +38,9 @@ export class SettingsBackupFacade {
|
||||
await waitForUiFeedbackFrame();
|
||||
|
||||
try {
|
||||
const backup = await this.playlistBackupService.exportBackup();
|
||||
const backup = await this.playlistBackupService.exportBackup({
|
||||
includeSecrets: this.includeSecrets(),
|
||||
});
|
||||
|
||||
if (this.runtime.supportsDesktopFileSave && window.electron) {
|
||||
const savePath = await window.electron.saveFileDialog(
|
||||
@@ -77,7 +89,11 @@ export class SettingsBackupFacade {
|
||||
|
||||
try {
|
||||
const summary = await this.playlistBackupService.importBackup(
|
||||
await file.text()
|
||||
await file.text(),
|
||||
{
|
||||
resolveXtreamCredentials: (request) =>
|
||||
this.resolveXtreamCredentials(request),
|
||||
}
|
||||
);
|
||||
|
||||
if (summary.imported > 0 || summary.merged > 0) {
|
||||
@@ -124,6 +140,36 @@ export class SettingsBackupFacade {
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
private async resolveXtreamCredentials(
|
||||
request: PlaylistBackupXtreamCredentialsRequest
|
||||
): Promise<PlaylistBackupXtreamCredentials | null> {
|
||||
const dialogRef = this.matDialog.open<
|
||||
SettingsBackupCredentialsDialogComponent,
|
||||
SettingsBackupCredentialsDialogData,
|
||||
PlaylistBackupXtreamCredentials | null
|
||||
>(SettingsBackupCredentialsDialogComponent, {
|
||||
data: {
|
||||
playlistTitle: request.title,
|
||||
serverHost: this.getServerHost(request.serverUrl),
|
||||
},
|
||||
maxWidth: 'calc(100vw - 32px)',
|
||||
width: '440px',
|
||||
});
|
||||
|
||||
return (await firstValueFrom(dialogRef.afterClosed())) ?? null;
|
||||
}
|
||||
|
||||
private getServerHost(serverUrl: string): string {
|
||||
try {
|
||||
const normalizedUrl = serverUrl.includes('://')
|
||||
? serverUrl
|
||||
: `https://${serverUrl}`;
|
||||
return new URL(normalizedUrl).host;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private buildBackupImportSummary(
|
||||
summary: PlaylistBackupImportSummary
|
||||
): string {
|
||||
|
||||
@@ -46,6 +46,8 @@ import {
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { SettingsComponent } from './settings.component';
|
||||
import { AppUpdateReleaseNotesDialogComponent } from './app-update-release-notes-dialog.component';
|
||||
import { SettingsBackupCredentialsDialogComponent } from './settings-backup-credentials-dialog.component';
|
||||
import { SettingsBackupSectionComponent } from './settings-backup-section.component';
|
||||
|
||||
import { signal } from '@angular/core';
|
||||
import { SettingsContextService } from '@iptvnator/workspace/shell/util';
|
||||
@@ -1192,9 +1194,125 @@ describe('SettingsComponent', () => {
|
||||
'/tmp/backup.json',
|
||||
'{}'
|
||||
);
|
||||
expect(playlistBackupService.exportBackup).toHaveBeenCalledWith({
|
||||
includeSecrets: false,
|
||||
});
|
||||
expect(component.isExportingData()).toBe(false);
|
||||
});
|
||||
|
||||
it('includes credentials only after the backup checkbox is enabled', async () => {
|
||||
const backupSection = fixture.debugElement.query(
|
||||
By.directive(SettingsBackupSectionComponent)
|
||||
).componentInstance as SettingsBackupSectionComponent;
|
||||
|
||||
expect(backupSection.includeSecrets()).toBe(false);
|
||||
|
||||
backupSection.includeSecrets.set(true);
|
||||
await component.exportData();
|
||||
|
||||
expect(playlistBackupService.exportBackup).toHaveBeenCalledWith({
|
||||
includeSecrets: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('shows both redacted-backup and secret-export guidance', () => {
|
||||
const backupSection = fixture.debugElement.query(
|
||||
By.directive(SettingsBackupSectionComponent)
|
||||
);
|
||||
|
||||
expect(backupSection.nativeElement.textContent).toContain(
|
||||
'SETTINGS.BACKUP_REDACTED_WARNING'
|
||||
);
|
||||
expect(backupSection.nativeElement.textContent).not.toContain(
|
||||
'SETTINGS.BACKUP_SECRETS_WARNING'
|
||||
);
|
||||
|
||||
(
|
||||
backupSection.componentInstance as SettingsBackupSectionComponent
|
||||
).includeSecrets.set(true);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(backupSection.nativeElement.textContent).toContain(
|
||||
'SETTINGS.BACKUP_SECRETS_WARNING'
|
||||
);
|
||||
});
|
||||
|
||||
it('resolves redacted Xtream credentials through the dialog and reports skipped entries', async () => {
|
||||
const backupJson = JSON.stringify({
|
||||
kind: 'iptvnator-playlist-backup',
|
||||
version: 1,
|
||||
exportedAt: '2026-04-21T00:00:00.000Z',
|
||||
includeSecrets: false,
|
||||
playlists: [],
|
||||
});
|
||||
const file = {
|
||||
text: jest.fn().mockResolvedValue(backupJson),
|
||||
} as unknown as File;
|
||||
const inputClickSpy = jest
|
||||
.spyOn(HTMLInputElement.prototype, 'click')
|
||||
.mockImplementation(function (this: HTMLInputElement) {
|
||||
Object.defineProperty(this, 'files', {
|
||||
configurable: true,
|
||||
value: [file],
|
||||
});
|
||||
this.dispatchEvent(new Event('change'));
|
||||
});
|
||||
const dialogOpenSpy = jest
|
||||
.spyOn(privateApi(component).matDialog, 'open')
|
||||
.mockReturnValue({
|
||||
afterClosed: () => of(undefined),
|
||||
} as unknown as ReturnType<MatDialog['open']>);
|
||||
(
|
||||
playlistBackupService.importBackup as jest.Mock
|
||||
).mockImplementationOnce(async (_json, options) => {
|
||||
const credentials = await options.resolveXtreamCredentials({
|
||||
exportedId: 'xtream-redacted',
|
||||
title: 'Living room',
|
||||
serverUrl:
|
||||
'https://viewer:secret@provider.example:8443/player_api.php?token=private',
|
||||
});
|
||||
|
||||
expect(credentials).toBeNull();
|
||||
|
||||
return {
|
||||
imported: 0,
|
||||
merged: 0,
|
||||
skipped: 1,
|
||||
failed: 0,
|
||||
errors: [],
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
component.importData();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(playlistBackupService.importBackup).toHaveBeenCalledWith(
|
||||
backupJson,
|
||||
{
|
||||
resolveXtreamCredentials: expect.any(Function),
|
||||
}
|
||||
);
|
||||
expect(dialogOpenSpy).toHaveBeenCalledWith(
|
||||
SettingsBackupCredentialsDialogComponent,
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
playlistTitle: 'Living room',
|
||||
serverHost: 'provider.example:8443',
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(snackBar.open).toHaveBeenCalledWith(
|
||||
expect.stringContaining('1 skipped'),
|
||||
undefined,
|
||||
expect.any(Object)
|
||||
);
|
||||
} finally {
|
||||
dialogOpenSpy.mockRestore();
|
||||
inputClickSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('falls back to browser backup download when desktop file-save preload is incomplete', async () => {
|
||||
fixture.destroy();
|
||||
const saveFileDialog = jest.fn().mockResolvedValue('/tmp/backup.json');
|
||||
|
||||
@@ -354,6 +354,22 @@
|
||||
"EXPORT_DATA": "تصدير",
|
||||
"EXPORTING_DATA": "جارٍ التصدير…",
|
||||
"REMOVE_ALL": "Delete all playlists and playlist data",
|
||||
"BACKUP_CONTENTS": "Backup contents",
|
||||
"BACKUP_INCLUDE_SECRETS": "Include portal credentials and Stalker device identity",
|
||||
"BACKUP_REDACTED_WARNING": "Portal passwords and Stalker device identity are excluded by default. Even a redacted backup can still contain MAC addresses, portal hosts/source URLs, and private URLs embedded in raw M3U playlist data.",
|
||||
"BACKUP_SECRETS_WARNING": "Warning: this export will include portal usernames and passwords plus Stalker device identity fields. Store the backup securely.",
|
||||
"BACKUP_CREDENTIALS_DIALOG": {
|
||||
"TITLE": "Xtream credentials required",
|
||||
"MESSAGE": "This redacted backup does not contain the credentials for this playlist.",
|
||||
"PLAYLIST": "Playlist",
|
||||
"SERVER": "Server",
|
||||
"USERNAME": "Username",
|
||||
"PASSWORD": "Password",
|
||||
"USERNAME_REQUIRED": "Enter a username.",
|
||||
"PASSWORD_REQUIRED": "Enter a password.",
|
||||
"SKIP": "Skip playlist",
|
||||
"IMPORT": "Import playlist"
|
||||
},
|
||||
"REMOVE_ALL_BUTTON": "Delete all",
|
||||
"REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.",
|
||||
"REMOVE_DIALOG": {
|
||||
|
||||
@@ -354,6 +354,22 @@
|
||||
"EXPORT_DATA": "صدّر",
|
||||
"EXPORTING_DATA": "جاري التصدير…",
|
||||
"REMOVE_ALL": "Delete all playlists and playlist data",
|
||||
"BACKUP_CONTENTS": "Backup contents",
|
||||
"BACKUP_INCLUDE_SECRETS": "Include portal credentials and Stalker device identity",
|
||||
"BACKUP_REDACTED_WARNING": "Portal passwords and Stalker device identity are excluded by default. Even a redacted backup can still contain MAC addresses, portal hosts/source URLs, and private URLs embedded in raw M3U playlist data.",
|
||||
"BACKUP_SECRETS_WARNING": "Warning: this export will include portal usernames and passwords plus Stalker device identity fields. Store the backup securely.",
|
||||
"BACKUP_CREDENTIALS_DIALOG": {
|
||||
"TITLE": "Xtream credentials required",
|
||||
"MESSAGE": "This redacted backup does not contain the credentials for this playlist.",
|
||||
"PLAYLIST": "Playlist",
|
||||
"SERVER": "Server",
|
||||
"USERNAME": "Username",
|
||||
"PASSWORD": "Password",
|
||||
"USERNAME_REQUIRED": "Enter a username.",
|
||||
"PASSWORD_REQUIRED": "Enter a password.",
|
||||
"SKIP": "Skip playlist",
|
||||
"IMPORT": "Import playlist"
|
||||
},
|
||||
"REMOVE_ALL_BUTTON": "Delete all",
|
||||
"REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.",
|
||||
"REMOVE_DIALOG": {
|
||||
|
||||
@@ -354,6 +354,22 @@
|
||||
"EXPORT_DATA": "Экспарт",
|
||||
"EXPORTING_DATA": "Экспартаванне...",
|
||||
"REMOVE_ALL": "Delete all playlists and playlist data",
|
||||
"BACKUP_CONTENTS": "Backup contents",
|
||||
"BACKUP_INCLUDE_SECRETS": "Include portal credentials and Stalker device identity",
|
||||
"BACKUP_REDACTED_WARNING": "Portal passwords and Stalker device identity are excluded by default. Even a redacted backup can still contain MAC addresses, portal hosts/source URLs, and private URLs embedded in raw M3U playlist data.",
|
||||
"BACKUP_SECRETS_WARNING": "Warning: this export will include portal usernames and passwords plus Stalker device identity fields. Store the backup securely.",
|
||||
"BACKUP_CREDENTIALS_DIALOG": {
|
||||
"TITLE": "Xtream credentials required",
|
||||
"MESSAGE": "This redacted backup does not contain the credentials for this playlist.",
|
||||
"PLAYLIST": "Playlist",
|
||||
"SERVER": "Server",
|
||||
"USERNAME": "Username",
|
||||
"PASSWORD": "Password",
|
||||
"USERNAME_REQUIRED": "Enter a username.",
|
||||
"PASSWORD_REQUIRED": "Enter a password.",
|
||||
"SKIP": "Skip playlist",
|
||||
"IMPORT": "Import playlist"
|
||||
},
|
||||
"REMOVE_ALL_BUTTON": "Delete all",
|
||||
"REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.",
|
||||
"REMOVE_DIALOG": {
|
||||
|
||||
@@ -354,6 +354,22 @@
|
||||
"EXPORT_DATA": "Export",
|
||||
"EXPORTING_DATA": "Wird exportiert …",
|
||||
"REMOVE_ALL": "Delete all playlists and playlist data",
|
||||
"BACKUP_CONTENTS": "Backup contents",
|
||||
"BACKUP_INCLUDE_SECRETS": "Include portal credentials and Stalker device identity",
|
||||
"BACKUP_REDACTED_WARNING": "Portal passwords and Stalker device identity are excluded by default. Even a redacted backup can still contain MAC addresses, portal hosts/source URLs, and private URLs embedded in raw M3U playlist data.",
|
||||
"BACKUP_SECRETS_WARNING": "Warning: this export will include portal usernames and passwords plus Stalker device identity fields. Store the backup securely.",
|
||||
"BACKUP_CREDENTIALS_DIALOG": {
|
||||
"TITLE": "Xtream credentials required",
|
||||
"MESSAGE": "This redacted backup does not contain the credentials for this playlist.",
|
||||
"PLAYLIST": "Playlist",
|
||||
"SERVER": "Server",
|
||||
"USERNAME": "Username",
|
||||
"PASSWORD": "Password",
|
||||
"USERNAME_REQUIRED": "Enter a username.",
|
||||
"PASSWORD_REQUIRED": "Enter a password.",
|
||||
"SKIP": "Skip playlist",
|
||||
"IMPORT": "Import playlist"
|
||||
},
|
||||
"REMOVE_ALL_BUTTON": "Delete all",
|
||||
"REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.",
|
||||
"REMOVE_DIALOG": {
|
||||
|
||||
@@ -354,6 +354,22 @@
|
||||
"EXPORT_DATA": "Εξαγωγή",
|
||||
"EXPORTING_DATA": "Εξαγωγή…",
|
||||
"REMOVE_ALL": "Delete all playlists and playlist data",
|
||||
"BACKUP_CONTENTS": "Backup contents",
|
||||
"BACKUP_INCLUDE_SECRETS": "Include portal credentials and Stalker device identity",
|
||||
"BACKUP_REDACTED_WARNING": "Portal passwords and Stalker device identity are excluded by default. Even a redacted backup can still contain MAC addresses, portal hosts/source URLs, and private URLs embedded in raw M3U playlist data.",
|
||||
"BACKUP_SECRETS_WARNING": "Warning: this export will include portal usernames and passwords plus Stalker device identity fields. Store the backup securely.",
|
||||
"BACKUP_CREDENTIALS_DIALOG": {
|
||||
"TITLE": "Xtream credentials required",
|
||||
"MESSAGE": "This redacted backup does not contain the credentials for this playlist.",
|
||||
"PLAYLIST": "Playlist",
|
||||
"SERVER": "Server",
|
||||
"USERNAME": "Username",
|
||||
"PASSWORD": "Password",
|
||||
"USERNAME_REQUIRED": "Enter a username.",
|
||||
"PASSWORD_REQUIRED": "Enter a password.",
|
||||
"SKIP": "Skip playlist",
|
||||
"IMPORT": "Import playlist"
|
||||
},
|
||||
"REMOVE_ALL_BUTTON": "Delete all",
|
||||
"REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.",
|
||||
"REMOVE_DIALOG": {
|
||||
|
||||
@@ -354,6 +354,22 @@
|
||||
"EXPORT_DATA": "Export",
|
||||
"EXPORTING_DATA": "Exporting…",
|
||||
"REMOVE_ALL": "Delete all playlists and playlist data",
|
||||
"BACKUP_CONTENTS": "Backup contents",
|
||||
"BACKUP_INCLUDE_SECRETS": "Include portal credentials and Stalker device identity",
|
||||
"BACKUP_REDACTED_WARNING": "Portal passwords and Stalker device identity are excluded by default. Even a redacted backup can still contain MAC addresses, portal hosts/source URLs, and private URLs embedded in raw M3U playlist data.",
|
||||
"BACKUP_SECRETS_WARNING": "Warning: this export will include portal usernames and passwords plus Stalker device identity fields. Store the backup securely.",
|
||||
"BACKUP_CREDENTIALS_DIALOG": {
|
||||
"TITLE": "Xtream credentials required",
|
||||
"MESSAGE": "This redacted backup does not contain the credentials for this playlist.",
|
||||
"PLAYLIST": "Playlist",
|
||||
"SERVER": "Server",
|
||||
"USERNAME": "Username",
|
||||
"PASSWORD": "Password",
|
||||
"USERNAME_REQUIRED": "Enter a username.",
|
||||
"PASSWORD_REQUIRED": "Enter a password.",
|
||||
"SKIP": "Skip playlist",
|
||||
"IMPORT": "Import playlist"
|
||||
},
|
||||
"REMOVE_ALL_BUTTON": "Delete all",
|
||||
"REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.",
|
||||
"REMOVE_DIALOG": {
|
||||
|
||||
@@ -354,6 +354,22 @@
|
||||
"EXPORT_DATA": "Exportar",
|
||||
"EXPORTING_DATA": "Exportando…",
|
||||
"REMOVE_ALL": "Delete all playlists and playlist data",
|
||||
"BACKUP_CONTENTS": "Backup contents",
|
||||
"BACKUP_INCLUDE_SECRETS": "Include portal credentials and Stalker device identity",
|
||||
"BACKUP_REDACTED_WARNING": "Portal passwords and Stalker device identity are excluded by default. Even a redacted backup can still contain MAC addresses, portal hosts/source URLs, and private URLs embedded in raw M3U playlist data.",
|
||||
"BACKUP_SECRETS_WARNING": "Warning: this export will include portal usernames and passwords plus Stalker device identity fields. Store the backup securely.",
|
||||
"BACKUP_CREDENTIALS_DIALOG": {
|
||||
"TITLE": "Xtream credentials required",
|
||||
"MESSAGE": "This redacted backup does not contain the credentials for this playlist.",
|
||||
"PLAYLIST": "Playlist",
|
||||
"SERVER": "Server",
|
||||
"USERNAME": "Username",
|
||||
"PASSWORD": "Password",
|
||||
"USERNAME_REQUIRED": "Enter a username.",
|
||||
"PASSWORD_REQUIRED": "Enter a password.",
|
||||
"SKIP": "Skip playlist",
|
||||
"IMPORT": "Import playlist"
|
||||
},
|
||||
"REMOVE_ALL_BUTTON": "Delete all",
|
||||
"REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.",
|
||||
"REMOVE_DIALOG": {
|
||||
|
||||
@@ -354,6 +354,22 @@
|
||||
"EXPORT_DATA": "Exporter",
|
||||
"EXPORTING_DATA": "Exportation…",
|
||||
"REMOVE_ALL": "Delete all playlists and playlist data",
|
||||
"BACKUP_CONTENTS": "Backup contents",
|
||||
"BACKUP_INCLUDE_SECRETS": "Include portal credentials and Stalker device identity",
|
||||
"BACKUP_REDACTED_WARNING": "Portal passwords and Stalker device identity are excluded by default. Even a redacted backup can still contain MAC addresses, portal hosts/source URLs, and private URLs embedded in raw M3U playlist data.",
|
||||
"BACKUP_SECRETS_WARNING": "Warning: this export will include portal usernames and passwords plus Stalker device identity fields. Store the backup securely.",
|
||||
"BACKUP_CREDENTIALS_DIALOG": {
|
||||
"TITLE": "Xtream credentials required",
|
||||
"MESSAGE": "This redacted backup does not contain the credentials for this playlist.",
|
||||
"PLAYLIST": "Playlist",
|
||||
"SERVER": "Server",
|
||||
"USERNAME": "Username",
|
||||
"PASSWORD": "Password",
|
||||
"USERNAME_REQUIRED": "Enter a username.",
|
||||
"PASSWORD_REQUIRED": "Enter a password.",
|
||||
"SKIP": "Skip playlist",
|
||||
"IMPORT": "Import playlist"
|
||||
},
|
||||
"REMOVE_ALL_BUTTON": "Delete all",
|
||||
"REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.",
|
||||
"REMOVE_DIALOG": {
|
||||
|
||||
@@ -354,6 +354,22 @@
|
||||
"EXPORT_DATA": "Exportálás",
|
||||
"EXPORTING_DATA": "Exportálás…",
|
||||
"REMOVE_ALL": "Az összes lejátszási lista és kapcsolódó adat törlése",
|
||||
"BACKUP_CONTENTS": "Backup contents",
|
||||
"BACKUP_INCLUDE_SECRETS": "Include portal credentials and Stalker device identity",
|
||||
"BACKUP_REDACTED_WARNING": "Portal passwords and Stalker device identity are excluded by default. Even a redacted backup can still contain MAC addresses, portal hosts/source URLs, and private URLs embedded in raw M3U playlist data.",
|
||||
"BACKUP_SECRETS_WARNING": "Warning: this export will include portal usernames and passwords plus Stalker device identity fields. Store the backup securely.",
|
||||
"BACKUP_CREDENTIALS_DIALOG": {
|
||||
"TITLE": "Xtream credentials required",
|
||||
"MESSAGE": "This redacted backup does not contain the credentials for this playlist.",
|
||||
"PLAYLIST": "Playlist",
|
||||
"SERVER": "Server",
|
||||
"USERNAME": "Username",
|
||||
"PASSWORD": "Password",
|
||||
"USERNAME_REQUIRED": "Enter a username.",
|
||||
"PASSWORD_REQUIRED": "Enter a password.",
|
||||
"SKIP": "Skip playlist",
|
||||
"IMPORT": "Import playlist"
|
||||
},
|
||||
"REMOVE_ALL_BUTTON": "Összes törlése",
|
||||
"REMOVE_ALL_DESCRIPTION": "Az alkalmazásban tárolt összes M3U-, Xtream- és Stalker-lejátszási lista törlése.",
|
||||
"REMOVE_DIALOG": {
|
||||
|
||||
@@ -354,6 +354,22 @@
|
||||
"EXPORT_DATA": "Esporta",
|
||||
"EXPORTING_DATA": "Esportazione...",
|
||||
"REMOVE_ALL": "Delete all playlists and playlist data",
|
||||
"BACKUP_CONTENTS": "Backup contents",
|
||||
"BACKUP_INCLUDE_SECRETS": "Include portal credentials and Stalker device identity",
|
||||
"BACKUP_REDACTED_WARNING": "Portal passwords and Stalker device identity are excluded by default. Even a redacted backup can still contain MAC addresses, portal hosts/source URLs, and private URLs embedded in raw M3U playlist data.",
|
||||
"BACKUP_SECRETS_WARNING": "Warning: this export will include portal usernames and passwords plus Stalker device identity fields. Store the backup securely.",
|
||||
"BACKUP_CREDENTIALS_DIALOG": {
|
||||
"TITLE": "Xtream credentials required",
|
||||
"MESSAGE": "This redacted backup does not contain the credentials for this playlist.",
|
||||
"PLAYLIST": "Playlist",
|
||||
"SERVER": "Server",
|
||||
"USERNAME": "Username",
|
||||
"PASSWORD": "Password",
|
||||
"USERNAME_REQUIRED": "Enter a username.",
|
||||
"PASSWORD_REQUIRED": "Enter a password.",
|
||||
"SKIP": "Skip playlist",
|
||||
"IMPORT": "Import playlist"
|
||||
},
|
||||
"REMOVE_ALL_BUTTON": "Delete all",
|
||||
"REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.",
|
||||
"REMOVE_DIALOG": {
|
||||
|
||||
@@ -354,6 +354,22 @@
|
||||
"EXPORT_DATA": "エクスポート",
|
||||
"EXPORTING_DATA": "エクスポート中…",
|
||||
"REMOVE_ALL": "Delete all playlists and playlist data",
|
||||
"BACKUP_CONTENTS": "Backup contents",
|
||||
"BACKUP_INCLUDE_SECRETS": "Include portal credentials and Stalker device identity",
|
||||
"BACKUP_REDACTED_WARNING": "Portal passwords and Stalker device identity are excluded by default. Even a redacted backup can still contain MAC addresses, portal hosts/source URLs, and private URLs embedded in raw M3U playlist data.",
|
||||
"BACKUP_SECRETS_WARNING": "Warning: this export will include portal usernames and passwords plus Stalker device identity fields. Store the backup securely.",
|
||||
"BACKUP_CREDENTIALS_DIALOG": {
|
||||
"TITLE": "Xtream credentials required",
|
||||
"MESSAGE": "This redacted backup does not contain the credentials for this playlist.",
|
||||
"PLAYLIST": "Playlist",
|
||||
"SERVER": "Server",
|
||||
"USERNAME": "Username",
|
||||
"PASSWORD": "Password",
|
||||
"USERNAME_REQUIRED": "Enter a username.",
|
||||
"PASSWORD_REQUIRED": "Enter a password.",
|
||||
"SKIP": "Skip playlist",
|
||||
"IMPORT": "Import playlist"
|
||||
},
|
||||
"REMOVE_ALL_BUTTON": "Delete all",
|
||||
"REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.",
|
||||
"REMOVE_DIALOG": {
|
||||
|
||||
@@ -354,6 +354,22 @@
|
||||
"EXPORT_DATA": "내보내다",
|
||||
"EXPORTING_DATA": "내보내는 중…",
|
||||
"REMOVE_ALL": "Delete all playlists and playlist data",
|
||||
"BACKUP_CONTENTS": "Backup contents",
|
||||
"BACKUP_INCLUDE_SECRETS": "Include portal credentials and Stalker device identity",
|
||||
"BACKUP_REDACTED_WARNING": "Portal passwords and Stalker device identity are excluded by default. Even a redacted backup can still contain MAC addresses, portal hosts/source URLs, and private URLs embedded in raw M3U playlist data.",
|
||||
"BACKUP_SECRETS_WARNING": "Warning: this export will include portal usernames and passwords plus Stalker device identity fields. Store the backup securely.",
|
||||
"BACKUP_CREDENTIALS_DIALOG": {
|
||||
"TITLE": "Xtream credentials required",
|
||||
"MESSAGE": "This redacted backup does not contain the credentials for this playlist.",
|
||||
"PLAYLIST": "Playlist",
|
||||
"SERVER": "Server",
|
||||
"USERNAME": "Username",
|
||||
"PASSWORD": "Password",
|
||||
"USERNAME_REQUIRED": "Enter a username.",
|
||||
"PASSWORD_REQUIRED": "Enter a password.",
|
||||
"SKIP": "Skip playlist",
|
||||
"IMPORT": "Import playlist"
|
||||
},
|
||||
"REMOVE_ALL_BUTTON": "Delete all",
|
||||
"REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.",
|
||||
"REMOVE_DIALOG": {
|
||||
|
||||
@@ -354,6 +354,22 @@
|
||||
"EXPORT_DATA": "Exporteren",
|
||||
"EXPORTING_DATA": "Exporteren…",
|
||||
"REMOVE_ALL": "Delete all playlists and playlist data",
|
||||
"BACKUP_CONTENTS": "Backup contents",
|
||||
"BACKUP_INCLUDE_SECRETS": "Include portal credentials and Stalker device identity",
|
||||
"BACKUP_REDACTED_WARNING": "Portal passwords and Stalker device identity are excluded by default. Even a redacted backup can still contain MAC addresses, portal hosts/source URLs, and private URLs embedded in raw M3U playlist data.",
|
||||
"BACKUP_SECRETS_WARNING": "Warning: this export will include portal usernames and passwords plus Stalker device identity fields. Store the backup securely.",
|
||||
"BACKUP_CREDENTIALS_DIALOG": {
|
||||
"TITLE": "Xtream credentials required",
|
||||
"MESSAGE": "This redacted backup does not contain the credentials for this playlist.",
|
||||
"PLAYLIST": "Playlist",
|
||||
"SERVER": "Server",
|
||||
"USERNAME": "Username",
|
||||
"PASSWORD": "Password",
|
||||
"USERNAME_REQUIRED": "Enter a username.",
|
||||
"PASSWORD_REQUIRED": "Enter a password.",
|
||||
"SKIP": "Skip playlist",
|
||||
"IMPORT": "Import playlist"
|
||||
},
|
||||
"REMOVE_ALL_BUTTON": "Delete all",
|
||||
"REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.",
|
||||
"REMOVE_DIALOG": {
|
||||
|
||||
@@ -354,6 +354,22 @@
|
||||
"EXPORT_DATA": "Eksportuj",
|
||||
"EXPORTING_DATA": "Eksportowanie…",
|
||||
"REMOVE_ALL": "Delete all playlists and playlist data",
|
||||
"BACKUP_CONTENTS": "Backup contents",
|
||||
"BACKUP_INCLUDE_SECRETS": "Include portal credentials and Stalker device identity",
|
||||
"BACKUP_REDACTED_WARNING": "Portal passwords and Stalker device identity are excluded by default. Even a redacted backup can still contain MAC addresses, portal hosts/source URLs, and private URLs embedded in raw M3U playlist data.",
|
||||
"BACKUP_SECRETS_WARNING": "Warning: this export will include portal usernames and passwords plus Stalker device identity fields. Store the backup securely.",
|
||||
"BACKUP_CREDENTIALS_DIALOG": {
|
||||
"TITLE": "Xtream credentials required",
|
||||
"MESSAGE": "This redacted backup does not contain the credentials for this playlist.",
|
||||
"PLAYLIST": "Playlist",
|
||||
"SERVER": "Server",
|
||||
"USERNAME": "Username",
|
||||
"PASSWORD": "Password",
|
||||
"USERNAME_REQUIRED": "Enter a username.",
|
||||
"PASSWORD_REQUIRED": "Enter a password.",
|
||||
"SKIP": "Skip playlist",
|
||||
"IMPORT": "Import playlist"
|
||||
},
|
||||
"REMOVE_ALL_BUTTON": "Delete all",
|
||||
"REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.",
|
||||
"REMOVE_DIALOG": {
|
||||
|
||||
@@ -354,6 +354,22 @@
|
||||
"EXPORT_DATA": "Exportar",
|
||||
"EXPORTING_DATA": "Exportando…",
|
||||
"REMOVE_ALL": "Delete all playlists and playlist data",
|
||||
"BACKUP_CONTENTS": "Backup contents",
|
||||
"BACKUP_INCLUDE_SECRETS": "Include portal credentials and Stalker device identity",
|
||||
"BACKUP_REDACTED_WARNING": "Portal passwords and Stalker device identity are excluded by default. Even a redacted backup can still contain MAC addresses, portal hosts/source URLs, and private URLs embedded in raw M3U playlist data.",
|
||||
"BACKUP_SECRETS_WARNING": "Warning: this export will include portal usernames and passwords plus Stalker device identity fields. Store the backup securely.",
|
||||
"BACKUP_CREDENTIALS_DIALOG": {
|
||||
"TITLE": "Xtream credentials required",
|
||||
"MESSAGE": "This redacted backup does not contain the credentials for this playlist.",
|
||||
"PLAYLIST": "Playlist",
|
||||
"SERVER": "Server",
|
||||
"USERNAME": "Username",
|
||||
"PASSWORD": "Password",
|
||||
"USERNAME_REQUIRED": "Enter a username.",
|
||||
"PASSWORD_REQUIRED": "Enter a password.",
|
||||
"SKIP": "Skip playlist",
|
||||
"IMPORT": "Import playlist"
|
||||
},
|
||||
"REMOVE_ALL_BUTTON": "Delete all",
|
||||
"REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.",
|
||||
"REMOVE_DIALOG": {
|
||||
|
||||
@@ -354,6 +354,22 @@
|
||||
"EXPORT_DATA": "Экспорт",
|
||||
"EXPORTING_DATA": "Экспорт…",
|
||||
"REMOVE_ALL": "Delete all playlists and playlist data",
|
||||
"BACKUP_CONTENTS": "Backup contents",
|
||||
"BACKUP_INCLUDE_SECRETS": "Include portal credentials and Stalker device identity",
|
||||
"BACKUP_REDACTED_WARNING": "Portal passwords and Stalker device identity are excluded by default. Even a redacted backup can still contain MAC addresses, portal hosts/source URLs, and private URLs embedded in raw M3U playlist data.",
|
||||
"BACKUP_SECRETS_WARNING": "Warning: this export will include portal usernames and passwords plus Stalker device identity fields. Store the backup securely.",
|
||||
"BACKUP_CREDENTIALS_DIALOG": {
|
||||
"TITLE": "Xtream credentials required",
|
||||
"MESSAGE": "This redacted backup does not contain the credentials for this playlist.",
|
||||
"PLAYLIST": "Playlist",
|
||||
"SERVER": "Server",
|
||||
"USERNAME": "Username",
|
||||
"PASSWORD": "Password",
|
||||
"USERNAME_REQUIRED": "Enter a username.",
|
||||
"PASSWORD_REQUIRED": "Enter a password.",
|
||||
"SKIP": "Skip playlist",
|
||||
"IMPORT": "Import playlist"
|
||||
},
|
||||
"REMOVE_ALL_BUTTON": "Delete all",
|
||||
"REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.",
|
||||
"REMOVE_DIALOG": {
|
||||
|
||||
@@ -354,6 +354,22 @@
|
||||
"EXPORT_DATA": "Dışa Aktar",
|
||||
"EXPORTING_DATA": "Dışa aktarılıyor…",
|
||||
"REMOVE_ALL": "Delete all playlists and playlist data",
|
||||
"BACKUP_CONTENTS": "Backup contents",
|
||||
"BACKUP_INCLUDE_SECRETS": "Include portal credentials and Stalker device identity",
|
||||
"BACKUP_REDACTED_WARNING": "Portal passwords and Stalker device identity are excluded by default. Even a redacted backup can still contain MAC addresses, portal hosts/source URLs, and private URLs embedded in raw M3U playlist data.",
|
||||
"BACKUP_SECRETS_WARNING": "Warning: this export will include portal usernames and passwords plus Stalker device identity fields. Store the backup securely.",
|
||||
"BACKUP_CREDENTIALS_DIALOG": {
|
||||
"TITLE": "Xtream credentials required",
|
||||
"MESSAGE": "This redacted backup does not contain the credentials for this playlist.",
|
||||
"PLAYLIST": "Playlist",
|
||||
"SERVER": "Server",
|
||||
"USERNAME": "Username",
|
||||
"PASSWORD": "Password",
|
||||
"USERNAME_REQUIRED": "Enter a username.",
|
||||
"PASSWORD_REQUIRED": "Enter a password.",
|
||||
"SKIP": "Skip playlist",
|
||||
"IMPORT": "Import playlist"
|
||||
},
|
||||
"REMOVE_ALL_BUTTON": "Delete all",
|
||||
"REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.",
|
||||
"REMOVE_DIALOG": {
|
||||
|
||||
@@ -354,6 +354,22 @@
|
||||
"EXPORT_DATA": "出口",
|
||||
"EXPORTING_DATA": "导出中…",
|
||||
"REMOVE_ALL": "Delete all playlists and playlist data",
|
||||
"BACKUP_CONTENTS": "Backup contents",
|
||||
"BACKUP_INCLUDE_SECRETS": "Include portal credentials and Stalker device identity",
|
||||
"BACKUP_REDACTED_WARNING": "Portal passwords and Stalker device identity are excluded by default. Even a redacted backup can still contain MAC addresses, portal hosts/source URLs, and private URLs embedded in raw M3U playlist data.",
|
||||
"BACKUP_SECRETS_WARNING": "Warning: this export will include portal usernames and passwords plus Stalker device identity fields. Store the backup securely.",
|
||||
"BACKUP_CREDENTIALS_DIALOG": {
|
||||
"TITLE": "Xtream credentials required",
|
||||
"MESSAGE": "This redacted backup does not contain the credentials for this playlist.",
|
||||
"PLAYLIST": "Playlist",
|
||||
"SERVER": "Server",
|
||||
"USERNAME": "Username",
|
||||
"PASSWORD": "Password",
|
||||
"USERNAME_REQUIRED": "Enter a username.",
|
||||
"PASSWORD_REQUIRED": "Enter a password.",
|
||||
"SKIP": "Skip playlist",
|
||||
"IMPORT": "Import playlist"
|
||||
},
|
||||
"REMOVE_ALL_BUTTON": "Delete all",
|
||||
"REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.",
|
||||
"REMOVE_DIALOG": {
|
||||
|
||||
@@ -354,6 +354,22 @@
|
||||
"EXPORT_DATA": "匯出",
|
||||
"EXPORTING_DATA": "正在匯出…",
|
||||
"REMOVE_ALL": "Delete all playlists and playlist data",
|
||||
"BACKUP_CONTENTS": "Backup contents",
|
||||
"BACKUP_INCLUDE_SECRETS": "Include portal credentials and Stalker device identity",
|
||||
"BACKUP_REDACTED_WARNING": "Portal passwords and Stalker device identity are excluded by default. Even a redacted backup can still contain MAC addresses, portal hosts/source URLs, and private URLs embedded in raw M3U playlist data.",
|
||||
"BACKUP_SECRETS_WARNING": "Warning: this export will include portal usernames and passwords plus Stalker device identity fields. Store the backup securely.",
|
||||
"BACKUP_CREDENTIALS_DIALOG": {
|
||||
"TITLE": "Xtream credentials required",
|
||||
"MESSAGE": "This redacted backup does not contain the credentials for this playlist.",
|
||||
"PLAYLIST": "Playlist",
|
||||
"SERVER": "Server",
|
||||
"USERNAME": "Username",
|
||||
"PASSWORD": "Password",
|
||||
"USERNAME_REQUIRED": "Enter a username.",
|
||||
"PASSWORD_REQUIRED": "Enter a password.",
|
||||
"SKIP": "Skip playlist",
|
||||
"IMPORT": "Import playlist"
|
||||
},
|
||||
"REMOVE_ALL_BUTTON": "Delete all",
|
||||
"REMOVE_ALL_DESCRIPTION": "Delete every M3U, Xtream, and Stalker playlist stored in the app.",
|
||||
"REMOVE_DIALOG": {
|
||||
|
||||
Reference in new issue
Block a user