mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
feat: export playlist as m3u
This commit is contained in:
1 parent
ba41c8dae5
commit
7e4d6b171f
8 files changed
+204
-179
No files matched your search
+159
-171
@@ -1,177 +1,165 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"cli": {
|
||||
"schematicCollections": [
|
||||
"@angular-eslint/schematics"
|
||||
]
|
||||
},
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"iptvnator": {
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:application": {
|
||||
"strict": true,
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-builders/custom-webpack:browser",
|
||||
"options": {
|
||||
"outputPath": "dist",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets",
|
||||
"src/manifest.webmanifest"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": [],
|
||||
"customWebpackConfig": {
|
||||
"path": "./angular.webpack.js",
|
||||
"replaceDuplicatePlugins": true
|
||||
},
|
||||
"serviceWorker": true,
|
||||
"ngswConfigPath": "ngsw-config.json",
|
||||
"webWorkerTsConfig": "tsconfig.worker.json"
|
||||
},
|
||||
"configurations": {
|
||||
"dev": {
|
||||
"optimization": false,
|
||||
"outputHashing": "none",
|
||||
"sourceMap": true,
|
||||
"namedChunks": false,
|
||||
"aot": false,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": false,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.dev.ts"
|
||||
}
|
||||
]
|
||||
},
|
||||
"web": {
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.web.ts"
|
||||
}
|
||||
]
|
||||
},
|
||||
"production": {
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-builders/custom-webpack:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "iptvnator:build"
|
||||
},
|
||||
"configurations": {
|
||||
"dev": {
|
||||
"browserTarget": "iptvnator:build:dev"
|
||||
},
|
||||
"web": {
|
||||
"browserTarget": "iptvnator:build:web"
|
||||
},
|
||||
"production": {
|
||||
"browserTarget": "iptvnator:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "iptvnator:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-builders/custom-webpack:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills-test.ts",
|
||||
"tsConfig": "src/tsconfig.spec.json",
|
||||
"karmaConfig": "src/karma.conf.js",
|
||||
"scripts": [],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"assets": [
|
||||
"src/assets"
|
||||
],
|
||||
"customWebpackConfig": {
|
||||
"path": "./angular.webpack.js",
|
||||
"replaceDuplicatePlugins": true
|
||||
},
|
||||
"webWorkerTsConfig": "tsconfig.worker.json"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.html"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"cli": {
|
||||
"schematicCollections": ["@angular-eslint/schematics"],
|
||||
"analytics": false
|
||||
},
|
||||
"iptvnator-e2e": {
|
||||
"root": "e2e",
|
||||
"projectType": "application",
|
||||
"architect": {
|
||||
"lint": {
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"e2e/**/*.ts"
|
||||
]
|
||||
}
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"iptvnator": {
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:application": {
|
||||
"strict": true,
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-builders/custom-webpack:browser",
|
||||
"options": {
|
||||
"outputPath": "dist",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets",
|
||||
"src/manifest.webmanifest"
|
||||
],
|
||||
"styles": ["src/styles.scss"],
|
||||
"scripts": [],
|
||||
"customWebpackConfig": {
|
||||
"path": "./angular.webpack.js",
|
||||
"replaceDuplicatePlugins": true
|
||||
},
|
||||
"serviceWorker": true,
|
||||
"ngswConfigPath": "ngsw-config.json",
|
||||
"webWorkerTsConfig": "tsconfig.worker.json"
|
||||
},
|
||||
"configurations": {
|
||||
"dev": {
|
||||
"optimization": false,
|
||||
"outputHashing": "none",
|
||||
"sourceMap": true,
|
||||
"namedChunks": false,
|
||||
"aot": false,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": false,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.dev.ts"
|
||||
}
|
||||
]
|
||||
},
|
||||
"web": {
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.web.ts"
|
||||
}
|
||||
]
|
||||
},
|
||||
"production": {
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-builders/custom-webpack:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "iptvnator:build"
|
||||
},
|
||||
"configurations": {
|
||||
"dev": {
|
||||
"browserTarget": "iptvnator:build:dev"
|
||||
},
|
||||
"web": {
|
||||
"browserTarget": "iptvnator:build:web"
|
||||
},
|
||||
"production": {
|
||||
"browserTarget": "iptvnator:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "iptvnator:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-builders/custom-webpack:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills-test.ts",
|
||||
"tsConfig": "src/tsconfig.spec.json",
|
||||
"karmaConfig": "src/karma.conf.js",
|
||||
"scripts": [],
|
||||
"styles": ["src/styles.scss"],
|
||||
"assets": ["src/assets"],
|
||||
"customWebpackConfig": {
|
||||
"path": "./angular.webpack.js",
|
||||
"replaceDuplicatePlugins": true
|
||||
},
|
||||
"webWorkerTsConfig": "tsconfig.worker.json"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"iptvnator-e2e": {
|
||||
"root": "e2e",
|
||||
"projectType": "application",
|
||||
"architect": {
|
||||
"lint": {
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": ["e2e/**/*.ts"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"prefix": "app",
|
||||
"style": "scss"
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"prefix": "app"
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"prefix": "app",
|
||||
"style": "scss"
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"prefix": "app"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,10 @@
|
||||
</ng-container>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-flat-button color="accent" (click)="exportPlaylist()">
|
||||
{{ 'HOME.PLAYLISTS.INFO_DIALOG.EXPORT_PLAYLIST' | translate }}
|
||||
</button>
|
||||
<div style="flex: 1 1 auto"></div>
|
||||
<button
|
||||
mat-flat-button
|
||||
mat-dialog-close
|
||||
|
||||
@@ -13,7 +13,8 @@ import { Actions } from '@ngrx/effects';
|
||||
import { provideMockActions } from '@ngrx/effects/testing';
|
||||
import { MockStore, provideMockStore } from '@ngrx/store/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { MockModule, MockPipe } from 'ng-mocks';
|
||||
import { MockModule, MockPipe, MockProvider } from 'ng-mocks';
|
||||
import { NgxIndexedDBModule } from 'ngx-indexed-db';
|
||||
import { Observable } from 'rxjs';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
import { ElectronServiceStub } from '../../../services/electron.service.stub';
|
||||
@@ -23,7 +24,6 @@ import { PlaylistInfoComponent } from './playlist-info.component';
|
||||
describe('PlaylistInfoComponent', () => {
|
||||
let component: PlaylistInfoComponent;
|
||||
let fixture: ComponentFixture<PlaylistInfoComponent>;
|
||||
let electronService: DataService;
|
||||
let mockStore: MockStore;
|
||||
const actions$ = new Observable<Actions>();
|
||||
|
||||
@@ -45,6 +45,7 @@ describe('PlaylistInfoComponent', () => {
|
||||
UntypedFormBuilder,
|
||||
provideMockStore(),
|
||||
provideMockActions(actions$),
|
||||
MockProvider(NgxIndexedDBModule),
|
||||
],
|
||||
}).compileComponents();
|
||||
})
|
||||
|
||||
@@ -6,16 +6,19 @@ import {
|
||||
ReactiveFormsModule,
|
||||
UntypedFormBuilder,
|
||||
UntypedFormGroup,
|
||||
Validators,
|
||||
Validators
|
||||
} from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatDialogModule, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { Playlist } from '../../../../../shared/playlist.interface';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
import { PlaylistsService } from '../../../services/playlists.service';
|
||||
import { PlaylistMeta } from '../../../shared/playlist-meta.type';
|
||||
import * as PlaylistActions from '../../../state/actions';
|
||||
|
||||
@@ -26,6 +29,7 @@ import * as PlaylistActions from '../../../state/actions';
|
||||
imports: [
|
||||
TranslateModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatCheckboxModule,
|
||||
CommonModule,
|
||||
@@ -49,6 +53,7 @@ export class PlaylistInfoComponent {
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private dataService: DataService,
|
||||
@Inject(MAT_DIALOG_DATA) playlist: Playlist,
|
||||
private playlistService: PlaylistsService,
|
||||
private store: Store
|
||||
) {
|
||||
this.playlist = playlist;
|
||||
@@ -89,4 +94,21 @@ export class PlaylistInfoComponent {
|
||||
saveChanges(playlist: PlaylistMeta): void {
|
||||
this.store.dispatch(PlaylistActions.updatePlaylistMeta({ playlist }));
|
||||
}
|
||||
|
||||
async exportPlaylist() {
|
||||
const playlistAsString = await firstValueFrom(
|
||||
this.playlistService.getRawPlaylistById(this.playlist._id)
|
||||
);
|
||||
const element = document.createElement('a');
|
||||
element.setAttribute(
|
||||
'href',
|
||||
'data:text/plain;charset=utf-8,' +
|
||||
encodeURIComponent(playlistAsString)
|
||||
);
|
||||
element.setAttribute('download', this.playlist.title || 'exported.m3u');
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,12 @@ import { Channel } from '../../../shared/channel.interface';
|
||||
import { GLOBAL_FAVORITES_PLAYLIST_ID } from '../../../shared/constants';
|
||||
import {
|
||||
Playlist,
|
||||
PlaylistUpdateState,
|
||||
PlaylistUpdateState
|
||||
} from '../../../shared/playlist.interface';
|
||||
import {
|
||||
aggregateFavoriteChannels,
|
||||
createFavoritesPlaylist,
|
||||
createPlaylistObject,
|
||||
createPlaylistObject
|
||||
} from '../../../shared/playlist.utils';
|
||||
import { DbStores } from '../indexed-db.config';
|
||||
import { PlaylistMeta } from '../shared/playlist-meta.type';
|
||||
@@ -191,4 +191,11 @@ export class PlaylistsService {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getRawPlaylistById(id: string) {
|
||||
return this.dbService.getByID<Playlist>(DbStores.Playlists, id).pipe(map(playlist => {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||
return playlist.playlist.header.raw + '\n' + playlist.playlist.items.map(item => item.raw).join('\n');
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,8 @@
|
||||
"UPDATE_FAILED": "Update fehlgeschlagen",
|
||||
"AUTO_UPDATE": "Auto-update",
|
||||
"AUTO_UPDATE_DESCRIPTION": "Wenn die Auto-Refresh-Funktion aktiviert ist, wird die Wiedergabeliste bei jedem Start der Anwendung automatisch aktualisiert.",
|
||||
"CUSTOM_USER_AGENT": "Einige IPTV-Anbieter benötigen einen bestimmten 'User-Agent', um ihre Playlist abspielen zu können."
|
||||
"CUSTOM_USER_AGENT": "Einige IPTV-Anbieter benötigen einen bestimmten 'User-Agent', um ihre Playlist abspielen zu können.",
|
||||
"EXPORT_PLAYLIST": "Als m3u exportieren"
|
||||
},
|
||||
"UPDATED": "Aktualisiert am",
|
||||
"REFRESH": "Aktualisieren",
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"UPDATE_FAILED": "Last playlist update failed",
|
||||
"AUTO_UPDATE": "Auto-update",
|
||||
"AUTO_UPDATE_DESCRIPTION": "If the auto-refresh function is activated, the playlist will be updated automatically each time the app is started.",
|
||||
"CUSTOM_USER_AGENT": "Some IPTV providers need a specific user-agent in order to play their playlist."
|
||||
"CUSTOM_USER_AGENT": "Some IPTV providers need a specific user-agent in order to play their playlist.",
|
||||
"EXPORT_PLAYLIST": "Export playlist as m3u"
|
||||
},
|
||||
"REMOVE_DIALOG": {
|
||||
"TITLE": "Remove playlist",
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"UPDATE_FAILED": "Не удалось обновить плейлист",
|
||||
"AUTO_UPDATE": "Автообновление",
|
||||
"AUTO_UPDATE_DESCRIPTION": "Если активирована функция автообновления, список воспроизведения будет обновляться автоматически при каждом запуске приложения.",
|
||||
"CUSTOM_USER_AGENT": "Некоторые IPTV провайдеры требуют указанние определенного пользовательского агента (User Agent) для воспроизведения своего плейлиста."
|
||||
"CUSTOM_USER_AGENT": "Некоторые IPTV провайдеры требуют указанние определенного пользовательского агента (User Agent) для воспроизведения своего плейлиста.",
|
||||
"EXPORT_PLAYLIST": "Экспортировать плейлист (m3u)"
|
||||
},
|
||||
"UPDATED": "Обновлен",
|
||||
"REFRESH": "Обновить плейлист",
|
||||
|
||||
Reference in new issue
Block a user