mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
Merge pull request #44 from 4gray/feature/auto-refresh-playlists-on-startup
Feature/auto refresh playlists on startup
This commit is contained in:
20 files changed
+440
-290
No files matched your search
+3
-2
@@ -11,7 +11,6 @@
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"prettier/@typescript-eslint",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/recommended-requiring-type-checking"
|
||||
],
|
||||
@@ -38,7 +37,8 @@
|
||||
"SwitchCase": 1,
|
||||
"CallExpression": { "arguments": "first" },
|
||||
"FunctionExpression": { "parameters": "first" },
|
||||
"FunctionDeclaration": { "parameters": "first" }
|
||||
"FunctionDeclaration": { "parameters": "first" },
|
||||
"ignoredNodes": ["TSTypeParameterInstantiation"]
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-empty-function": 0,
|
||||
@@ -49,6 +49,7 @@
|
||||
"@typescript-eslint/no-unsafe-assignment": 0,
|
||||
"@typescript-eslint/no-unsafe-return": 0,
|
||||
"@typescript-eslint/no-floating-promises": 0,
|
||||
"@typescript-eslint/unbound-method": ["off"],
|
||||
"@angular-eslint/use-injectable-provided-in": "error",
|
||||
"@angular-eslint/no-attribute-decorator": "error"
|
||||
}
|
||||
|
||||
@@ -6,3 +6,4 @@
|
||||
npx stylelint "**/*.scss"
|
||||
npx --no-install commitlint --edit
|
||||
npm test -- --coverage
|
||||
npx eslint src/**/*.ts
|
||||
@@ -45,23 +45,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"web": {
|
||||
"optimization": false,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": true,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": false,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": false,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.web.ts"
|
||||
}
|
||||
]
|
||||
},
|
||||
"production": {
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { ParsedPlaylist } from './src/typings.d';
|
||||
import { app, BrowserWindow, ipcMain } from 'electron';
|
||||
import { parse } from 'iptv-playlist-parser';
|
||||
import axios from 'axios';
|
||||
import { guid } from '@datorama/akita';
|
||||
import { Playlist } from './src/app/home/playlist.interface';
|
||||
import Nedb from 'nedb-promises-ts';
|
||||
import {
|
||||
Playlist,
|
||||
PlaylistUpdateState,
|
||||
} from './src/app/home/playlist.interface';
|
||||
import Nedb, { Cursor } from 'nedb-promises-ts';
|
||||
import {
|
||||
EPG_ERROR,
|
||||
EPG_FETCH,
|
||||
@@ -36,9 +40,9 @@ export class Api {
|
||||
workerWindow: BrowserWindow;
|
||||
|
||||
constructor() {
|
||||
ipcMain.on('parse-playlist-by-url', async (event, args) => {
|
||||
ipcMain.on('parse-playlist-by-url', (event, args) => {
|
||||
try {
|
||||
await axios.get(args.url).then((result) => {
|
||||
axios.get(args.url).then((result) => {
|
||||
const parsedPlaylist = this.convertFileStringToPlaylist(
|
||||
result.data
|
||||
);
|
||||
@@ -77,55 +81,63 @@ export class Api {
|
||||
|
||||
ipcMain.on('playlists-all', (event) => this.sendAllPlaylists(event));
|
||||
|
||||
ipcMain.on('playlist-by-id', async (event, args) => {
|
||||
const playlist = await db.findOne({ _id: args.id });
|
||||
this.setUserAgent(playlist.userAgent);
|
||||
event.sender.send(PLAYLIST_PARSE_RESPONSE, {
|
||||
payload: playlist,
|
||||
ipcMain.on('playlist-by-id', (event, args) => {
|
||||
db.findOne({ _id: args.id }).then((playlist) => {
|
||||
this.setUserAgent(playlist.userAgent);
|
||||
event.sender.send(PLAYLIST_PARSE_RESPONSE, {
|
||||
payload: playlist,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on('playlist-remove-by-id', async (event, args) => {
|
||||
const removed = await db.remove({ _id: args.id });
|
||||
if (removed) {
|
||||
event.sender.send('playlist-remove-by-id-result', {
|
||||
message: 'playlist was removed',
|
||||
});
|
||||
}
|
||||
ipcMain.on('playlist-remove-by-id', (event, args) => {
|
||||
db.remove({ _id: args.id }).then((removed) => {
|
||||
if (removed) {
|
||||
event.sender.send('playlist-remove-by-id-result', {
|
||||
message: 'playlist was removed',
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// open playlist from file system
|
||||
ipcMain.on('open-file', (event, args) => {
|
||||
fs.readFile(args.filePath, 'utf-8', (err, data: string) => {
|
||||
if (err) {
|
||||
console.log(
|
||||
'An error ocurred reading the file :' + err.message
|
||||
);
|
||||
return;
|
||||
}
|
||||
fs.readFile(
|
||||
args.filePath,
|
||||
'utf-8',
|
||||
(err: NodeJS.ErrnoException, data: string) => {
|
||||
if (err) {
|
||||
console.log(
|
||||
'An error ocurred reading the file :' + err.message
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedPlaylist = this.convertFileStringToPlaylist(data);
|
||||
const playlistObject = this.createPlaylistObject(
|
||||
args.fileName,
|
||||
parsedPlaylist,
|
||||
args.filePath,
|
||||
'FILE'
|
||||
);
|
||||
this.insertToDb(playlistObject);
|
||||
event.sender.send(PLAYLIST_PARSE_RESPONSE, {
|
||||
payload: playlistObject,
|
||||
});
|
||||
});
|
||||
const parsedPlaylist =
|
||||
this.convertFileStringToPlaylist(data);
|
||||
const playlistObject = this.createPlaylistObject(
|
||||
args.fileName,
|
||||
parsedPlaylist,
|
||||
args.filePath,
|
||||
'FILE'
|
||||
);
|
||||
this.insertToDb(playlistObject);
|
||||
event.sender.send(PLAYLIST_PARSE_RESPONSE, {
|
||||
payload: playlistObject,
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
ipcMain.on('update-favorites', async (event, args) => {
|
||||
const updated = await db.update(
|
||||
ipcMain.on('update-favorites', (event, args) => {
|
||||
db.update(
|
||||
{ id: args.id },
|
||||
{ $set: { favorites: args.favorites } }
|
||||
);
|
||||
if (!updated.numAffected || updated.numAffected === 0) {
|
||||
console.error('Error: Favorites were not updated');
|
||||
}
|
||||
).then((updated) => {
|
||||
if (!updated.numAffected || updated.numAffected === 0) {
|
||||
console.error('Error: Favorites were not updated');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// listeners for EPG events
|
||||
@@ -146,22 +158,29 @@ export class Api {
|
||||
this.mainWindow.webContents.send(EPG_ERROR, arg)
|
||||
);
|
||||
|
||||
ipcMain.on(PLAYLIST_SAVE_DETAILS, async (event, args) => {
|
||||
const updated = await db.update(
|
||||
{ _id: args._id },
|
||||
{
|
||||
$set: {
|
||||
title: args.title,
|
||||
userAgent: args.userAgent,
|
||||
autoRefresh: args.autoRefresh,
|
||||
},
|
||||
}
|
||||
);
|
||||
if (!updated.numAffected || updated.numAffected === 0) {
|
||||
console.error('Error: Playlist details were not updated');
|
||||
ipcMain.on(
|
||||
PLAYLIST_SAVE_DETAILS,
|
||||
(
|
||||
event,
|
||||
args: Pick<
|
||||
Playlist,
|
||||
'_id' | 'title' | 'userAgent' | 'autoRefresh'
|
||||
>
|
||||
) => {
|
||||
this.updatePlaylistById(args._id, {
|
||||
title: args.title,
|
||||
userAgent: args.userAgent,
|
||||
autoRefresh: args.autoRefresh,
|
||||
}).then((updated) => {
|
||||
if (!updated.numAffected || updated.numAffected === 0) {
|
||||
console.error(
|
||||
'Error: Playlist details were not updated'
|
||||
);
|
||||
}
|
||||
this.sendAllPlaylists(event);
|
||||
});
|
||||
}
|
||||
this.sendAllPlaylists(event);
|
||||
});
|
||||
);
|
||||
|
||||
ipcMain.on(
|
||||
PLAYLIST_UPDATE,
|
||||
@@ -173,16 +192,52 @@ export class Api {
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.refreshPlaylists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the update process for all the playlists with the enabled auto-refresh flag
|
||||
*/
|
||||
refreshPlaylists(): void {
|
||||
this.getAllPlaylistsMeta().then((playlists) => {
|
||||
playlists.forEach((playlist) => {
|
||||
if (playlist.autoRefresh && playlist.autoRefresh === true) {
|
||||
if (playlist.url) {
|
||||
this.fetchPlaylistByUrl(playlist._id, playlist.url);
|
||||
} else if (playlist.filePath) {
|
||||
this.fetchPlaylistByFilePath(
|
||||
playlist._id,
|
||||
playlist.filePath
|
||||
);
|
||||
} else {
|
||||
console.log('skip...');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends list with all playlists which are stored in the database
|
||||
* @param event main event
|
||||
*/
|
||||
async sendAllPlaylists(event: Electron.IpcMainEvent): Promise<void> {
|
||||
const playlists = await db.find(
|
||||
{ type: { $exists: false } },
|
||||
{
|
||||
sendAllPlaylists(event: Electron.IpcMainEvent): void {
|
||||
this.getAllPlaylistsMeta().then((playlists) => {
|
||||
event.sender.send('playlist-all-result', {
|
||||
payload: playlists,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all existing playlists with meta information from the database
|
||||
* @returns
|
||||
*/
|
||||
getAllPlaylistsMeta(): Cursor<Playlist> {
|
||||
return db
|
||||
.find({ type: { $exists: false } })
|
||||
.projection({
|
||||
count: 1,
|
||||
title: 1,
|
||||
_id: 1,
|
||||
@@ -193,11 +248,9 @@ export class Api {
|
||||
filePath: 1,
|
||||
autoRefresh: 1,
|
||||
updateDate: 1,
|
||||
}
|
||||
);
|
||||
event.sender.send('playlist-all-result', {
|
||||
payload: playlists,
|
||||
});
|
||||
updateState: 1,
|
||||
})
|
||||
.sort({ importDate: -1 });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -280,11 +333,33 @@ export class Api {
|
||||
playlist,
|
||||
count: playlist.items.length,
|
||||
updateDate: Date.now(),
|
||||
updateState: PlaylistUpdateState.UPDATED,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the provided playlist in the database
|
||||
* @param id id of the playlist
|
||||
* @param data playlist data to update
|
||||
* @returns
|
||||
*/
|
||||
updatePlaylistById(
|
||||
id: string,
|
||||
data: Partial<Playlist>
|
||||
): Promise<{
|
||||
numAffected: number;
|
||||
upsert: boolean;
|
||||
}> {
|
||||
return db.update(
|
||||
{ _id: id },
|
||||
{
|
||||
$set: data,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the fetched playlist string to the playlist object, updates it in the database and sends the updated playlists array back to the renderer
|
||||
* @param id id of the playlist to update
|
||||
@@ -294,20 +369,28 @@ export class Api {
|
||||
async handlePlaylistRefresh(
|
||||
id: string,
|
||||
playlistString: any,
|
||||
event: Electron.IpcMainEvent
|
||||
event?: Electron.IpcMainEvent
|
||||
): Promise<void> {
|
||||
const playlist = this.convertFileStringToPlaylist(playlistString);
|
||||
const updated = await this.saveUpdatedPlaylist(id, playlist);
|
||||
const playlist: ParsedPlaylist =
|
||||
this.convertFileStringToPlaylist(playlistString);
|
||||
const updated = await this.updatePlaylistById(id, {
|
||||
playlist,
|
||||
count: playlist.items.length,
|
||||
updateDate: Date.now(),
|
||||
updateState: PlaylistUpdateState.UPDATED,
|
||||
});
|
||||
if (!updated.numAffected || updated.numAffected === 0) {
|
||||
console.error('Error: Playlist details were not updated');
|
||||
}
|
||||
|
||||
event.sender.send(PLAYLIST_UPDATE_RESPONSE, {
|
||||
message: `Success! The playlist was successfully updated (${playlist.items.length} channels)`,
|
||||
});
|
||||
if (event) {
|
||||
event.sender.send(PLAYLIST_UPDATE_RESPONSE, {
|
||||
message: `Success! The playlist was successfully updated (${playlist.items.length} channels)`,
|
||||
});
|
||||
|
||||
// send all playlists back to the renderer process
|
||||
this.sendAllPlaylists(event);
|
||||
// send all playlists back to the renderer process
|
||||
this.sendAllPlaylists(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -319,7 +402,7 @@ export class Api {
|
||||
async fetchPlaylistByUrl(
|
||||
id: string,
|
||||
url: string,
|
||||
event: Electron.IpcMainEvent
|
||||
event?: Electron.IpcMainEvent
|
||||
): Promise<void> {
|
||||
try {
|
||||
await axios
|
||||
@@ -328,8 +411,11 @@ export class Api {
|
||||
this.handlePlaylistRefresh(id, result.data, event)
|
||||
);
|
||||
} catch (err) {
|
||||
this.updatePlaylistById(id, {
|
||||
updateState: PlaylistUpdateState.NOT_UPDATED,
|
||||
});
|
||||
event.sender.send(ERROR, {
|
||||
message: `${err.response.statusText}. Please check the entered playlist URL again.`,
|
||||
message: `File not found. Please check the entered playlist URL again.`,
|
||||
status: err.response.status,
|
||||
});
|
||||
}
|
||||
@@ -344,19 +430,19 @@ export class Api {
|
||||
fetchPlaylistByFilePath(
|
||||
id: string,
|
||||
path: string,
|
||||
event: Electron.IpcMainEvent
|
||||
event?: Electron.IpcMainEvent
|
||||
): void {
|
||||
try {
|
||||
fs.readFile(path, 'utf-8', async (err, data) => {
|
||||
fs.readFile(path, 'utf-8', (err, data) => {
|
||||
if (err) {
|
||||
this.handleFileNotFoundError(err, event);
|
||||
this.handleFileNotFoundError(err, id, event);
|
||||
return;
|
||||
}
|
||||
|
||||
this.handlePlaylistRefresh(id, data, event);
|
||||
});
|
||||
} catch (err) {
|
||||
this.handleFileNotFoundError(err, event);
|
||||
this.handleFileNotFoundError(err, id, event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,13 +458,21 @@ export class Api {
|
||||
syscall: string;
|
||||
path: string;
|
||||
},
|
||||
event: Electron.IpcMainEvent
|
||||
id: string,
|
||||
event?: Electron.IpcMainEvent
|
||||
) {
|
||||
console.error(error);
|
||||
event.sender.send(ERROR, {
|
||||
message: `Sorry, playlist was not found (${error.path})`,
|
||||
status: 'ENOENT',
|
||||
this.updatePlaylistById(id, {
|
||||
updateState: PlaylistUpdateState.NOT_UPDATED,
|
||||
});
|
||||
if (event) {
|
||||
// send all playlists back to the renderer process
|
||||
this.sendAllPlaylists(event);
|
||||
event.sender.send(ERROR, {
|
||||
message: `Sorry, playlist was not found (${error.path})`,
|
||||
status: 'ENOENT',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
convertFileStringToPlaylist(m3uString: string): any {
|
||||
@@ -398,7 +492,7 @@ export class Api {
|
||||
* Inserts new playlist to the database
|
||||
* @param playlist playlist to add
|
||||
*/
|
||||
insertToDb(playlist: any): void {
|
||||
insertToDb(playlist: Playlist): void {
|
||||
db.insert(playlist).then((response) => {
|
||||
console.log('playlist was saved...', response._id);
|
||||
});
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
"!e2e/",
|
||||
"!hooks/",
|
||||
"!angular.json",
|
||||
"!_config.yml",
|
||||
"!karma.conf.js",
|
||||
"!tsconfig.json",
|
||||
"!tslint.json"
|
||||
],
|
||||
|
||||
+154
-154
@@ -1,157 +1,157 @@
|
||||
{
|
||||
"name": "iptvnator",
|
||||
"version": "0.6.0",
|
||||
"description": "IPTV player application.",
|
||||
"homepage": "https://github.com/4gray/iptvnator",
|
||||
"author": {
|
||||
"name": "4gray",
|
||||
"email": "dev@artslab.info"
|
||||
},
|
||||
"keywords": [
|
||||
"angular",
|
||||
"electron",
|
||||
"typescript",
|
||||
"m3u",
|
||||
"m3u8",
|
||||
"player",
|
||||
"iptv",
|
||||
"video",
|
||||
"tv"
|
||||
],
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"postinstall": "electron-builder install-app-deps && husky install",
|
||||
"ng": "ng",
|
||||
"start": "npm-run-all -p electron:serve ng:serve",
|
||||
"build": "npm run electron:serve-tsc && ng build --base-href ./",
|
||||
"build:dev": "npm run build -- -c dev",
|
||||
"build:prod": "npm run build -- -c production",
|
||||
"ng:serve": "ng serve",
|
||||
"ng:serve:web": "ng serve -c web -o",
|
||||
"electron:serve-tsc": "tsc -p tsconfig.serve.json",
|
||||
"electron:serve": "wait-on tcp:4200 && npm run electron:serve-tsc && npx electron . --serve",
|
||||
"electron:local": "npm run build:prod && npx electron .",
|
||||
"electron:build": "npm run build:prod && electron-builder build",
|
||||
"electron:build:mac": "npm run build:prod && electron-builder build -m",
|
||||
"electron:build:linux": "npm run build:prod && electron-builder build -l --x64 --ia32 --arm64 --armv7l",
|
||||
"electron:build:windows": "npm run build:prod && electron-builder build -w",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"e2e": "npm run build:prod && cross-env TS_NODE_PROJECT='e2e/tsconfig.e2e.json' mocha e2e/**/*.ts --config ./.mocharc.json --exit",
|
||||
"e2e:watch": "npm run e2e -- --watch",
|
||||
"version": "conventional-changelog -i CHANGELOG.md -p angular -s -r 0 && git add CHANGELOG.md",
|
||||
"lint": "ng lint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/custom-webpack": "9.2.0",
|
||||
"@angular-devkit/build-angular": "0.1101.4",
|
||||
"@angular-eslint/builder": "0.0.1-alpha.32",
|
||||
"@angular-eslint/eslint-plugin": "0.0.1-alpha.32",
|
||||
"@angular-eslint/eslint-plugin-template": "0.0.1-alpha.32",
|
||||
"@angular-eslint/template-parser": "0.0.1-alpha.32",
|
||||
"@angular/cli": "11.1.4",
|
||||
"@angular/common": "11.1.2",
|
||||
"@angular/compiler": "11.1.2",
|
||||
"@angular/compiler-cli": "11.1.2",
|
||||
"@angular/core": "11.1.2",
|
||||
"@angular/forms": "11.1.2",
|
||||
"@angular/language-service": "11.1.2",
|
||||
"@angular/platform-browser": "11.1.2",
|
||||
"@angular/platform-browser-dynamic": "11.1.2",
|
||||
"@angular/router": "11.1.2",
|
||||
"@commitlint/cli": "11.0.0",
|
||||
"@commitlint/config-conventional": "11.0.0",
|
||||
"@ngx-translate/core": "13.0.0",
|
||||
"@ngx-translate/http-loader": "6.0.0",
|
||||
"@semantic-release/changelog": "5.0.1",
|
||||
"@semantic-release/git": "9.0.0",
|
||||
"@semantic-release/npm": "7.1.1",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
"@types/jasminewd2": "2.0.8",
|
||||
"@types/jest": "26.0.14",
|
||||
"@types/mocha": "7.0.2",
|
||||
"@types/node": "12.12.6",
|
||||
"@typescript-eslint/eslint-plugin": "4.11.1",
|
||||
"@typescript-eslint/eslint-plugin-tslint": "4.11.1",
|
||||
"@typescript-eslint/parser": "4.11.1",
|
||||
"chai": "4.2.0",
|
||||
"conventional-changelog-cli": "2.0.34",
|
||||
"core-js": "3.6.5",
|
||||
"cross-env": "7.0.2",
|
||||
"electron": "10.2.0",
|
||||
"electron-builder": "22.9.1",
|
||||
"eslint": "7.13.0",
|
||||
"eslint-plugin-import": "2.22.1",
|
||||
"husky": "5.0.9",
|
||||
"jasmine-core": "~3.6.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
"jest": "26.4.2",
|
||||
"jest-preset-angular": "8.3.1",
|
||||
"karma": "5.2.3",
|
||||
"karma-coverage-istanbul-reporter": "3.0.3",
|
||||
"karma-electron": "6.3.0",
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"mocha": "7.2.0",
|
||||
"ng-mocks": "11.5.0",
|
||||
"npm-run-all": "4.1.5",
|
||||
"prettier": "2.3.0",
|
||||
"rxjs": "6.6.3",
|
||||
"semantic-release": "17.2.3",
|
||||
"spectron": "12.0.0",
|
||||
"stylelint": "13.13.1",
|
||||
"stylelint-config-standard": "22.0.0",
|
||||
"stylelint-scss": "3.19.0",
|
||||
"ts-node": "8.10.2",
|
||||
"tslib": "2.1.0",
|
||||
"typescript": "4.1.3",
|
||||
"wait-on": "5.2.1",
|
||||
"webdriver-manager": "12.1.7",
|
||||
"zone.js": "~0.11.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "11.1.2",
|
||||
"@angular/cdk": "11.1.2",
|
||||
"@angular/flex-layout": "10.0.0-beta.32",
|
||||
"@angular/material": "11.1.2",
|
||||
"@datorama/akita": "5.2.6",
|
||||
"@ngx-pwa/local-storage": "11.1.0",
|
||||
"@types/hls.js": "0.13.3",
|
||||
"@types/video.js": "7.3.11",
|
||||
"@videojs/http-streaming": "2.3.1",
|
||||
"about-window": "1.13.4",
|
||||
"axios": "0.21.1",
|
||||
"custom-electron-titlebar": "3.2.5",
|
||||
"epg-parser": "0.1.1",
|
||||
"hls.js": "0.14.16",
|
||||
"iptv-playlist-parser": "0.6.0",
|
||||
"lodash": "4.17.21",
|
||||
"material-icons": "0.3.1",
|
||||
"moment": "2.29.1",
|
||||
"nedb": "1.8.0",
|
||||
"nedb-promises-ts": "0.0.1-4",
|
||||
"ngx-filter-pipe": "2.1.2",
|
||||
"ngx-uploader": "11.0.0",
|
||||
"ngx-whats-new": "0.0.1",
|
||||
"semver": "7.3.4",
|
||||
"uuid": "8.3.0",
|
||||
"video-js": "0.7.0"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-preset-angular",
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/src/setupJest.ts"
|
||||
"name": "iptvnator",
|
||||
"version": "0.6.0",
|
||||
"description": "IPTV player application.",
|
||||
"homepage": "https://github.com/4gray/iptvnator",
|
||||
"author": {
|
||||
"name": "4gray",
|
||||
"email": "dev@artslab.info"
|
||||
},
|
||||
"keywords": [
|
||||
"angular",
|
||||
"electron",
|
||||
"typescript",
|
||||
"m3u",
|
||||
"m3u8",
|
||||
"player",
|
||||
"iptv",
|
||||
"video",
|
||||
"tv"
|
||||
],
|
||||
"testMatch": [
|
||||
"**/+(*.)+(spec|test).+(ts)?(x)"
|
||||
],
|
||||
"coverageReporters": [
|
||||
"html",
|
||||
"lcov"
|
||||
]
|
||||
}
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"postinstall": "electron-builder install-app-deps && husky install",
|
||||
"ng": "ng",
|
||||
"start": "npm-run-all -p electron:serve ng:serve",
|
||||
"build": "npm run electron:serve-tsc && ng build --base-href ./",
|
||||
"build:dev": "npm run build -- -c dev",
|
||||
"build:prod": "npm run build -- -c production",
|
||||
"ng:serve": "ng serve",
|
||||
"ng:serve:web": "ng serve -c web -o",
|
||||
"electron:serve-tsc": "tsc -p tsconfig.serve.json",
|
||||
"electron:serve": "wait-on tcp:4200 && npm run electron:serve-tsc && npx electron . --serve",
|
||||
"electron:local": "npm run build:prod && npx electron .",
|
||||
"electron:build": "npm run build:prod && electron-builder build",
|
||||
"electron:build:mac": "npm run build:prod && electron-builder build -m",
|
||||
"electron:build:linux": "npm run build:prod && electron-builder build -l --x64 --ia32 --arm64 --armv7l",
|
||||
"electron:build:windows": "npm run build:prod && electron-builder build -w",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"e2e": "npm run build:prod && cross-env TS_NODE_PROJECT='e2e/tsconfig.e2e.json' mocha e2e/**/*.ts --config ./.mocharc.json --exit",
|
||||
"e2e:watch": "npm run e2e -- --watch",
|
||||
"version": "conventional-changelog -i CHANGELOG.md -p angular -s -r 0 && git add CHANGELOG.md",
|
||||
"lint": "ng lint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/custom-webpack": "9.2.0",
|
||||
"@angular-devkit/build-angular": "0.1101.4",
|
||||
"@angular-eslint/builder": "0.0.1-alpha.32",
|
||||
"@angular-eslint/eslint-plugin": "0.0.1-alpha.32",
|
||||
"@angular-eslint/eslint-plugin-template": "0.0.1-alpha.32",
|
||||
"@angular-eslint/template-parser": "0.0.1-alpha.32",
|
||||
"@angular/cli": "11.1.4",
|
||||
"@angular/common": "11.1.2",
|
||||
"@angular/compiler": "11.1.2",
|
||||
"@angular/compiler-cli": "11.1.2",
|
||||
"@angular/core": "11.1.2",
|
||||
"@angular/forms": "11.1.2",
|
||||
"@angular/language-service": "11.1.2",
|
||||
"@angular/platform-browser": "11.1.2",
|
||||
"@angular/platform-browser-dynamic": "11.1.2",
|
||||
"@angular/router": "11.1.2",
|
||||
"@commitlint/cli": "11.0.0",
|
||||
"@commitlint/config-conventional": "11.0.0",
|
||||
"@ngx-translate/core": "13.0.0",
|
||||
"@ngx-translate/http-loader": "6.0.0",
|
||||
"@semantic-release/changelog": "5.0.1",
|
||||
"@semantic-release/git": "9.0.0",
|
||||
"@semantic-release/npm": "7.1.1",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
"@types/jasminewd2": "2.0.8",
|
||||
"@types/jest": "26.0.14",
|
||||
"@types/mocha": "7.0.2",
|
||||
"@types/node": "12.12.6",
|
||||
"@typescript-eslint/eslint-plugin": "4.11.1",
|
||||
"@typescript-eslint/eslint-plugin-tslint": "4.11.1",
|
||||
"@typescript-eslint/parser": "4.11.1",
|
||||
"chai": "4.2.0",
|
||||
"conventional-changelog-cli": "2.0.34",
|
||||
"core-js": "3.6.5",
|
||||
"cross-env": "7.0.2",
|
||||
"electron": "10.2.0",
|
||||
"electron-builder": "22.9.1",
|
||||
"eslint": "7.13.0",
|
||||
"eslint-plugin-import": "2.22.1",
|
||||
"husky": "5.0.9",
|
||||
"jasmine-core": "~3.6.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
"jest": "26.4.2",
|
||||
"jest-preset-angular": "8.3.1",
|
||||
"karma": "5.2.3",
|
||||
"karma-coverage-istanbul-reporter": "3.0.3",
|
||||
"karma-electron": "6.3.0",
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"mocha": "7.2.0",
|
||||
"ng-mocks": "11.5.0",
|
||||
"npm-run-all": "4.1.5",
|
||||
"prettier": "2.3.0",
|
||||
"rxjs": "6.6.3",
|
||||
"semantic-release": "17.2.3",
|
||||
"spectron": "12.0.0",
|
||||
"stylelint": "13.13.1",
|
||||
"stylelint-config-standard": "22.0.0",
|
||||
"stylelint-scss": "3.19.0",
|
||||
"ts-node": "8.10.2",
|
||||
"tslib": "2.1.0",
|
||||
"typescript": "4.1.3",
|
||||
"wait-on": "5.2.1",
|
||||
"webdriver-manager": "12.1.7",
|
||||
"zone.js": "~0.11.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "11.1.2",
|
||||
"@angular/cdk": "11.1.2",
|
||||
"@angular/flex-layout": "10.0.0-beta.32",
|
||||
"@angular/material": "11.1.2",
|
||||
"@datorama/akita": "5.2.6",
|
||||
"@ngx-pwa/local-storage": "11.1.0",
|
||||
"@types/hls.js": "0.13.3",
|
||||
"@types/video.js": "7.3.11",
|
||||
"@videojs/http-streaming": "2.3.1",
|
||||
"about-window": "1.13.4",
|
||||
"axios": "0.21.1",
|
||||
"custom-electron-titlebar": "3.2.5",
|
||||
"epg-parser": "0.1.1",
|
||||
"hls.js": "0.14.16",
|
||||
"iptv-playlist-parser": "0.6.0",
|
||||
"lodash": "4.17.21",
|
||||
"material-icons": "0.3.1",
|
||||
"moment": "2.29.1",
|
||||
"nedb": "1.8.0",
|
||||
"nedb-promises-ts": "0.0.1-4",
|
||||
"ngx-filter-pipe": "2.1.2",
|
||||
"ngx-uploader": "11.0.0",
|
||||
"ngx-whats-new": "0.0.1",
|
||||
"semver": "7.3.4",
|
||||
"uuid": "8.3.0",
|
||||
"video-js": "0.7.0"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-preset-angular",
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/src/setupJest.ts"
|
||||
],
|
||||
"testMatch": [
|
||||
"**/+(*.)+(spec|test).+(ts)?(x)"
|
||||
],
|
||||
"coverageReporters": [
|
||||
"html",
|
||||
"lcov"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -41,18 +41,18 @@ export class HomeComponent {
|
||||
commandsList = [
|
||||
{
|
||||
id: PLAYLIST_PARSE_RESPONSE,
|
||||
execute: (response: any) => this.setPlaylist(response.payload),
|
||||
execute: (response: { payload: Playlist }) =>
|
||||
this.setPlaylist(response.payload),
|
||||
},
|
||||
{
|
||||
id: 'playlist-all-result',
|
||||
execute: (response: { payload: Partial<Playlist[]> }) =>
|
||||
(this.playlists = response.payload.sort((a, b) =>
|
||||
b.importDate.localeCompare(a.importDate)
|
||||
)),
|
||||
execute: (response: { payload: Partial<PlaylistMeta[]> }): void => {
|
||||
this.playlists = response.payload;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'playlist-remove-by-id-result',
|
||||
execute: () => {
|
||||
execute: (): void => {
|
||||
this.snackBar.open('Done! Playlist was removed.', null, {
|
||||
duration: 2000,
|
||||
});
|
||||
@@ -61,7 +61,7 @@ export class HomeComponent {
|
||||
},
|
||||
{
|
||||
id: ERROR,
|
||||
execute: (response: { message: string; status: number }) => {
|
||||
execute: (response: { message: string; status: number }): void => {
|
||||
this.isLoading = false;
|
||||
this.showNotification(
|
||||
`Error: ${response.status} ${response.message}.`
|
||||
|
||||
@@ -26,6 +26,6 @@ export interface Playlist {
|
||||
userAgent?: string;
|
||||
filePath?: string;
|
||||
autoRefresh: boolean;
|
||||
updateDate?: string;
|
||||
updateDate?: number;
|
||||
updateState?: PlaylistUpdateState;
|
||||
}
|
||||
@@ -42,6 +42,27 @@
|
||||
}}</mat-label>
|
||||
<input formControlName="userAgent" matInput />
|
||||
</mat-form-field>
|
||||
<mat-form-field class="full-width" *ngIf="playlist.filePath">
|
||||
<mat-label>
|
||||
{{ 'HOME.PLAYLISTS.INFO_DIALOG.FILE_PATH' | translate }}
|
||||
</mat-label>
|
||||
<input formControlName="filePath" matInput />
|
||||
<mat-hint *ngIf="playlist.updateState === 2">
|
||||
{{ 'HOME.PLAYLISTS.INFO_DIALOG.UPDATE_FAILED' | translate }}
|
||||
</mat-hint>
|
||||
</mat-form-field>
|
||||
<mat-checkbox
|
||||
*ngIf="playlist.url || playlist.filePath"
|
||||
class="full-width"
|
||||
formControlName="autoRefresh"
|
||||
>
|
||||
{{ 'HOME.PLAYLISTS.INFO_DIALOG.AUTO_UPDATE' | translate }}
|
||||
</mat-checkbox>
|
||||
<p>
|
||||
{{
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.AUTO_UPDATE_DESCRIPTION' | translate
|
||||
}}
|
||||
</p>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { PlaylistInfoComponent } from './playlist-info.component';
|
||||
import { FormsModule, ReactiveFormsModule, FormBuilder } from '@angular/forms';
|
||||
import { Playlist } from '../../playlist.interface';
|
||||
|
||||
describe('PlaylistInfoComponent', () => {
|
||||
let component: PlaylistInfoComponent;
|
||||
@@ -51,7 +52,7 @@ describe('PlaylistInfoComponent', () => {
|
||||
});
|
||||
|
||||
it('should send an event to the main process after save', () => {
|
||||
const playlistToSave = { _id: 'a12345', title: 'Playlist' };
|
||||
const playlistToSave = { _id: 'a12345', title: 'Playlist' } as Playlist;
|
||||
spyOn(electronService.ipcRenderer, 'send');
|
||||
component.saveChanges(playlistToSave);
|
||||
expect(electronService.ipcRenderer.send).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -61,6 +61,11 @@ export class PlaylistInfoComponent {
|
||||
disabled: true,
|
||||
}),
|
||||
url: new FormControl({ value: this.playlist.url, disabled: true }),
|
||||
filePath: new FormControl({
|
||||
value: this.playlist.filePath,
|
||||
disabled: true,
|
||||
}),
|
||||
autoRefresh: new FormControl(this.playlist.autoRefresh),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -68,7 +73,9 @@ export class PlaylistInfoComponent {
|
||||
* Saves updated playlist information
|
||||
* @param data updated form data
|
||||
*/
|
||||
saveChanges(data: Pick<Playlist, '_id' | 'title' | 'userAgent'>): void {
|
||||
saveChanges(
|
||||
data: Pick<Playlist, '_id' | 'title' | 'userAgent' | 'autoRefresh'>
|
||||
): void {
|
||||
this.electronService.ipcRenderer.send(PLAYLIST_SAVE_DETAILS, data);
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,15 @@
|
||||
| {{ 'HOME.PLAYLISTS.UPDATED' | translate }}:
|
||||
{{ item.updateDate | date: 'MMMM d, yyyy, HH:mm' }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="item.updateState === 2">
|
||||
<mat-icon
|
||||
[matTooltip]="
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.UPDATE_FAILED' | translate
|
||||
"
|
||||
[inline]="true"
|
||||
>warning</mat-icon
|
||||
>
|
||||
</ng-container>
|
||||
</div>
|
||||
<button
|
||||
*ngIf="item.url || item.filePath"
|
||||
|
||||
@@ -57,7 +57,11 @@
|
||||
"TITLE": "Titel",
|
||||
"USER_AGENT": "User agent",
|
||||
"SAVE": "Speichern",
|
||||
"ORIGINAL_FILENAME": "Original-Dateinamen"
|
||||
"ORIGINAL_FILENAME": "Original-Dateinamen",
|
||||
"FILE_PATH": "Dateipfad",
|
||||
"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."
|
||||
},
|
||||
"UPDATED": "Aktualisiert am",
|
||||
"REFRESH": "Aktualisieren"
|
||||
|
||||
@@ -25,7 +25,11 @@
|
||||
"CLOSE": "Close",
|
||||
"SAVE": "Save",
|
||||
"USER_AGENT": "User agent",
|
||||
"ORIGINAL_FILENAME": "Original filename"
|
||||
"ORIGINAL_FILENAME": "Original filename",
|
||||
"FILE_PATH": "File path",
|
||||
"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."
|
||||
},
|
||||
"UPDATED": "Updated",
|
||||
"REFRESH": "Refresh playlist"
|
||||
|
||||
@@ -25,7 +25,11 @@
|
||||
"CLOSE": "Закрыть",
|
||||
"SAVE": "Сохранить",
|
||||
"USER_AGENT": "User agent",
|
||||
"ORIGINAL_FILENAME": "Имя файла при загрузке"
|
||||
"ORIGINAL_FILENAME": "Имя файла при загрузке",
|
||||
"FILE_PATH": "Путь к файлу",
|
||||
"UPDATE_FAILED": "Не удалось обновить плейлист",
|
||||
"AUTO_UPDATE": "Автообновление",
|
||||
"AUTO_UPDATE_DESCRIPTION": "Если активирована функция автообновления, список воспроизведения будет обновляться автоматически при каждом запуске приложения."
|
||||
},
|
||||
"UPDATED": "Обновлен",
|
||||
"REFRESH": "Обновить плейлист"
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
// The list of which env maps to which file can be found in `.angular-cli.json`.
|
||||
|
||||
export const AppConfig = {
|
||||
production: false,
|
||||
environment: 'DEV'
|
||||
production: false,
|
||||
environment: 'DEV',
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
export const AppConfig = {
|
||||
production: true,
|
||||
environment: 'PROD'
|
||||
production: true,
|
||||
environment: 'PROD',
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
// The file contents for the current environment will overwrite these during build.
|
||||
// The build system defaults to the dev environment which uses `index.ts`, but if you do
|
||||
// `ng build --env=prod` then `index.prod.ts` will be used instead.
|
||||
// The list of which env maps to which file can be found in `.angular-cli.json`.
|
||||
|
||||
export const AppConfig = {
|
||||
production: false,
|
||||
environment: 'DEV'
|
||||
};
|
||||
+5
-5
@@ -5,11 +5,11 @@ import { AppModule } from './app/app.module';
|
||||
import { AppConfig } from './environments/environment';
|
||||
|
||||
if (AppConfig.production) {
|
||||
enableProdMode();
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic()
|
||||
.bootstrapModule(AppModule, {
|
||||
preserveWhitespaces: false
|
||||
})
|
||||
.catch(err => console.error(err));
|
||||
.bootstrapModule(AppModule, {
|
||||
preserveWhitespaces: false,
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
Vendored
+32
@@ -7,3 +7,35 @@ interface Window {
|
||||
process: any;
|
||||
require: any;
|
||||
}
|
||||
|
||||
export interface ParsedPlaylist {
|
||||
header: {
|
||||
attrs: {
|
||||
'x-tvg-url': string;
|
||||
};
|
||||
raw: string;
|
||||
};
|
||||
items: ParsedPlaylistItem[];
|
||||
}
|
||||
|
||||
export interface ParsedPlaylistItem {
|
||||
name: string;
|
||||
tvg: {
|
||||
id: string;
|
||||
name: string;
|
||||
language: string;
|
||||
country: string;
|
||||
url: string;
|
||||
logo: string;
|
||||
rec: string;
|
||||
};
|
||||
group: {
|
||||
title: string;
|
||||
};
|
||||
http: {
|
||||
referrer: string;
|
||||
'user-agent': string;
|
||||
};
|
||||
url: string;
|
||||
raw: string;
|
||||
}
|
||||
Reference in new issue
Block a user