Fillers can be set up to import playlists or collections from Plex automatically. Filler Service refactor.

This commit is contained in:
vexorian 2025-12-14 23:12:57 -04:00
parent caf3b3b72c
commit 2541e42513
10 changed files with 501 additions and 84 deletions

View File

@ -17,6 +17,7 @@ const HDHR = require('./src/hdhr')
const FileCacheService = require('./src/services/file-cache-service');
const CacheImageService = require('./src/services/cache-image-service');
const ChannelService = require("./src/services/channel-service");
const FillerService = require("./src/services/filler-service");
const xmltv = require('./src/xmltv')
const Plex = require('./src/plex');
@ -104,7 +105,7 @@ initDB(db, channelDB)
channelService = new ChannelService(channelDB);
fillerDB = new FillerDB( path.join(process.env.DATABASE, 'filler') , channelService );
let fillerDB = new FillerDB( path.join(process.env.DATABASE, 'filler') );
customShowDB = new CustomShowDB( path.join(process.env.DATABASE, 'custom-shows') );
let programPlayTimeDB = new ProgramPlayTimeDB( path.join(process.env.DATABASE, 'play-cache') );
let ffmpegSettingsService = new FfmpegSettingsService(db);
@ -134,6 +135,9 @@ activeChannelService = new ActiveChannelService(onDemandService, channelService)
eventService = new EventService();
let fillerService = new FillerService(fillerDB, plexProxyService,
channelService);
i18next
.use(i18nextBackend)
.use(i18nextMiddleware.LanguageDetector)
@ -323,12 +327,12 @@ app.use('/favicon.svg', express.static(
app.use('/custom.css', express.static(path.join(process.env.DATABASE, 'custom.css')))
// API Routers
app.use(api.router(db, channelService, fillerDB, customShowDB, xmltvInterval, guideService, m3uService, eventService, ffmpegSettingsService, plexServerDB, plexProxyService, ffmpegInfo))
app.use(api.router(db, channelService, fillerDB, customShowDB, xmltvInterval, guideService, m3uService, eventService, ffmpegSettingsService, plexServerDB, plexProxyService, ffmpegInfo, fillerService))
app.use('/api/cache/images', cacheImageService.apiRouters())
app.use('/' + fontAwesome, express.static(path.join(process.env.DATABASE, fontAwesome)))
app.use('/' + bootstrap, express.static(path.join(process.env.DATABASE, bootstrap)))
app.use(video.router( channelService, fillerDB, db, programmingService, activeChannelService, programPlayTimeDB, ffmpegInfo ))
app.use(video.router( channelService, fillerService, db, programmingService, activeChannelService, programPlayTimeDB, ffmpegInfo ))
app.use(hdhr.router)
app.listen(process.env.PORT, () => {
console.log(`HTTP server running on port: http://*:${process.env.PORT}`)

View File

@ -22,7 +22,7 @@ function safeString(object) {
}
module.exports = { router: api }
function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideService, _m3uService, eventService, ffmpegSettingsService, plexServerDB, plexProxyService, ffmpegInfo ) {
function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideService, _m3uService, eventService, ffmpegSettingsService, plexServerDB, plexProxyService, ffmpegInfo, fillerService ) {
let m3uService = _m3uService;
const router = express.Router()
@ -419,7 +419,7 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
if (typeof(id) === 'undefined') {
return res.status(400).send("Missing id");
}
await fillerDB.saveFiller(id, req.body );
await fillerService.saveFiller(id, req.body );
return res.status(204).send({});
} catch(err) {
console.error(err);
@ -428,7 +428,7 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
})
router.put('/api/filler', async (req, res) => {
try {
let uuid = await fillerDB.createFiller(req.body );
let uuid = await fillerService.createFiller(req.body );
return res.status(201).send({id: uuid});
} catch(err) {
console.error(err);
@ -441,7 +441,7 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
if (typeof(id) === 'undefined') {
return res.status(400).send("Missing id");
}
await fillerDB.deleteFiller(id);
await fillerService.deleteFiller(id);
return res.status(204).send({});
} catch(err) {
console.error(err);
@ -455,7 +455,7 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
if (typeof(id) === 'undefined') {
return res.status(400).send("Missing id");
}
let channels = await fillerDB.getFillerChannels(id);
let channels = await fillerService.getFillerChannels(id);
if (channels == null) {
return res.status(404).send("Filler not found");
}

View File

@ -4,12 +4,9 @@ let fs = require('fs');
class FillerDB {
constructor(folder, channelService) {
constructor(folder) {
this.folder = folder;
this.cache = {};
this.channelService = channelService;
}
async $loadFiller(id) {
@ -77,40 +74,8 @@ class FillerDB {
return id;
}
async getFillerChannels(id) {
let numbers = await this.channelService.getAllChannelNumbers();
let channels = [];
await Promise.all( numbers.map( async(number) => {
let ch = await this.channelService.getChannel(number);
let name = ch.name;
let fillerCollections = ch.fillerCollections;
for (let i = 0 ; i < fillerCollections.length; i++) {
if (fillerCollections[i].id === id) {
channels.push( {
number: number,
name : name,
} );
break;
}
}
ch = null;
} ) );
return channels;
}
async deleteFiller(id) {
try {
let channels = await this.getFillerChannels(id);
await Promise.all( channels.map( async(channel) => {
console.log(`Updating channel ${channel.number} , remove filler: ${id}`);
let json = await channelService.getChannel(channel.number);
json.fillerCollections = json.fillerCollections.filter( (col) => {
return col.id != id;
} );
await this.channelService.saveChannel( channel.number, json );
} ) );
let f = path.join(this.folder, `${id}.json` );
await new Promise( (resolve, reject) => {
fs.unlink(f, function (err) {
@ -162,27 +127,6 @@ class FillerDB {
} );
}
async getFillersFromChannel(channel) {
let loadChannelFiller = async(fillerEntry) => {
let content = [];
try {
let filler = await this.getFiller(fillerEntry.id);
content = filler.content;
} catch(e) {
console.error(`Channel #${channel.number} - ${channel.name} references an unattainable filler id: ${fillerEntry.id}`);
}
return {
id: fillerEntry.id,
content: content,
weight: fillerEntry.weight,
cooldown: fillerEntry.cooldown,
}
};
return await Promise.all(
channel.fillerCollections.map(loadChannelFiller)
);
}
}

View File

@ -20,7 +20,7 @@
const path = require('path');
var fs = require('fs');
const TARGET_VERSION = 900;
const TARGET_VERSION = 1000;
const STEPS = [
// [v, v2, x] : if the current version is v, call x(db), and version becomes v2
@ -46,6 +46,7 @@ const STEPS = [
[ 803, 900, (db) => fixFFMpegPathSetting(db) ],
[ 804, 900, (db) => fixFFMpegPathSetting(db) ],
[ 805, 900, (db) => fixFFMpegPathSetting(db) ],
[ 900, 1000, () => fixFillerModes() ],
]
const { v4: uuidv4 } = require('uuid');
@ -679,6 +680,25 @@ function extractFillersFromChannels() {
}
function fixFillerModes() {
console.log("Fixing filler modes...");
let fillers = path.join(process.env.DATABASE, 'filler');
let fillerFiles = fs.readdirSync(fillers);
for (let i = 0; i < fillerFiles.length; i++) {
if (path.extname( fillerFiles[i] ) === '.json') {
console.log("Migrating filler : " + fillerFiles[i] +"..." );
let fillerPath = path.join(fillers, fillerFiles[i]);
let filler = JSON.parse(fs.readFileSync(fillerPath, 'utf-8'));
if ( typeof(filler.mode) !== "string" ) {
filler.mode = "custom";
}
fs.writeFileSync( fillerPath, JSON.stringify(filler), 'utf-8');
}
}
console.log("Done fixing filler modes.");
}
function addFPS(db) {
let ffmpegSettings = db['ffmpeg-settings'].find()[0];
let f = path.join(process.env.DATABASE, 'ffmpeg-settings.json');

View File

@ -0,0 +1,150 @@
const events = require('events')
const FILLER_UPDATE = 30 * 60 * 1000; //30 minutes might be too aggressive
//this will be configurable one day.
class FillerService extends events.EventEmitter {
constructor(fillerDB, plexProxyService, channelService) {
super();
this.fillerDB = fillerDB;
this.plexProxyService = plexProxyService;
this.channelService = channelService;
}
async saveFiller(id, body) {
body = await this.prework(body);
return this.fillerDB.saveFiller(id, body);
}
async createFiller(body) {
body = await this.prework(body);
return this.fillerDB.createFiller(body);
}
async getFillerChannels(id) {
let numbers = await this.channelService.getAllChannelNumbers();
let channels = [];
await Promise.all( numbers.map( async(number) => {
let ch = await this.channelService.getChannel(number);
let name = ch.name;
let fillerCollections = ch.fillerCollections;
for (let i = 0 ; i < fillerCollections.length; i++) {
if (fillerCollections[i].id === id) {
channels.push( {
number: number,
name : name,
} );
break;
}
}
ch = null;
} ) );
return channels;
}
async deleteFiller(id) {
try {
let channels = await this.getFillerChannels(id);
await Promise.all( channels.map( async(channel) => {
console.log(`Updating channel ${channel.number} , remove filler: ${id}`);
let json = await channelService.getChannel(channel.number);
json.fillerCollections = json.fillerCollections.filter( (col) => {
return col.id != id;
} );
await this.channelService.saveChannel( channel.number, json );
} ) );
} finally {
await this.fillerDB.deleteFiller(id);
}
}
async prework(body) {
if (body.mode === "import") {
body.content = await this.getContents(body);
body.import.lastRefreshTime = new Date().getTime();
} else {
delete body.import;
}
return body;
}
async getContents(body) {
let serverKey = body.import.serverName;
let key = body.import.key;
let content = await this.plexProxyService.getKeyMediaContents(serverKey, key);
console.log(JSON.stringify(content));
return content;
}
async getFillersFromChannel(channel) {
let loadChannelFiller = async(fillerEntry) => {
let content = [];
try {
let filler = await this.fillerDB.getFiller(fillerEntry.id);
await this.fillerUsageWatcher(fillerEntry.id, filler);
content = filler.content;
} catch(e) {
console.error(`Channel #${channel.number} - ${channel.name} references an unattainable filler id: ${fillerEntry.id}`, e);
}
return {
id: fillerEntry.id,
content: content,
weight: fillerEntry.weight,
cooldown: fillerEntry.cooldown,
}
};
return await Promise.all(
channel.fillerCollections.map(loadChannelFiller)
);
}
async fillerUsageWatcher(id, filler) {
if (filler.mode === "import") {
//I need to upgrade nodejs version ASAP
let lastTime = 0;
if (
(typeof(filler.import) !== "undefined")
&&
!isNaN(filler.import.lastRefreshTime)
) {
lastTime = filler.import.lastRefreshTime;
}
let t = new Date().getTime();
if ( t - lastTime >= FILLER_UPDATE) {
//time to do an update.
if ( (typeof(filler.content) === "undefined")
|| (filler.content.length == 0)
) {
//It should probably be an sync update...
await this.refreshFiller(id);
} else {
this.refreshFiller(id);
}
}
}
}
async refreshFiller(id) {
let t0 = new Date().getTime();
console.log(`Refreshing filler with id=${id}`);
try {
let filler = await this.fillerDB.getFiller(id);
await this.saveFiller(id, filler);
} catch (err) {
console.log(`Unable to update filler: ${id}`, err);
} finally {
let t1 = new Date().getTime();
console.log(`Refreshed filler with id=${id} in ${t1-t0}ms`);
}
}
}
module.exports = FillerService

View File

@ -9,22 +9,86 @@ class PlexProxyService extends events.EventEmitter {
}
async get(serverName64, path) {
let plexServer = await getPlexServer(this.plexServerDB, serverName64);
let plexServer = await getPlexServer64(this.plexServerDB, serverName64);
// A potential area of improvement is to reuse the client when possible
let client = new Plex(plexServer);
return { MediaContainer: await client.Get("/" + path) };
}
async getKeyMediaContents(serverName, key) {
let plexServer = await getPlexServer(this.plexServerDB, serverName);
let client = new Plex(plexServer);
let obj = { MediaContainer: await client.Get(key) };
let metadata = obj.MediaContainer.Metadata;
if ( typeof(metadata) !== "object") {
return [];
}
metadata = metadata.map( (item) => fillerMapper(serverName, item) );
return metadata;
}
}
function fillerMapper(serverName, plexMetadata) {
let image = {};
if ( (typeof(plexMetadata.Image) === "object")
&& (typeof(plexMetadata.Image[0]) === "object")
) {
image = plexMetadata.Image[0];
}
let media = {};
if ( (typeof(plexMetadata.Media) === "object")
&& (typeof(plexMetadata.Media[0]) === "object")
) {
media = plexMetadata.Media[0];
}
async function getPlexServer(plexServerDB, serverName64) {
let serverKey = Buffer.from(serverName64, 'base64').toString('utf-8');
let part = {};
if ( (typeof(media.Part) === "object")
&& (typeof(media.Part[0]) === "object")
) {
part = media.Part[0];
}
return {
title : plexMetadata.title,
key : plexMetadata.key,
ratingKey: plexMetadata.ratingKey,
icon : image.url,
type : plexMetadata.type,
duration : part.duration,
durationStr : undefined,
summary : "",
date : "",
year : plexMetadata.year,
plexFile : part.key,
file : part.file,
showTitle: plexMetadata.title,
episode : 1,
season : 1,
serverKey: serverName,
commercials: [],
}
}
async function getPlexServer(plexServerDB, serverKey) {
let server = await plexServerDB.getPlexServerByName(serverKey);
if (server == null) {
throw Error("server not found");
}
return server;
}
async function getPlexServer64(plexServerDB, serverName64) {
let serverKey = Buffer.from(serverName64, 'base64').toString('utf-8');
return await getPlexServer(plexServerDB, serverKey);
}
module.exports = PlexProxyService

View File

@ -18,7 +18,7 @@ async function shutdown() {
stopPlayback = true;
}
function video( channelService, fillerDB, db, programmingService, activeChannelService, programPlayTimeDB, ffmpegInfo ) {
function video( channelService, fillerService, db, programmingService, activeChannelService, programPlayTimeDB, ffmpegInfo ) {
var router = express.Router()
router.get('/setup', async (req, res) => {
@ -304,7 +304,7 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
if ( (prog == null) || (typeof(prog) === 'undefined') || (prog.program == null) || (typeof(prog.program) == "undefined") ) {
throw "No video to play, this means there's a serious unexpected bug or the channel db is corrupted."
}
let fillers = await fillerDB.getFillersFromChannel(brandChannel);
let fillers = await fillerService.getFillersFromChannel(brandChannel);
try {
let lineup = helperFuncs.createLineup(programPlayTimeDB, prog, brandChannel, fillers, isFirst)
lineupItem = lineup.shift();

View File

@ -1,4 +1,4 @@
module.exports = function ($timeout, commonProgramTools, getShowData) {
module.exports = function ($timeout, dizquetv, commonProgramTools, getShowData) {
return {
restrict: 'E',
templateUrl: 'templates/filler-config.html',
@ -13,6 +13,16 @@ module.exports = function ($timeout, commonProgramTools, getShowData) {
scope.content = [];
scope.visible = false;
scope.error = undefined;
scope.modes = [ {
name: "import",
description: "Collection/Playlist from Plex",
}, {
name: "custom",
description: "Custom List of Clips",
} ];
scope.servers = [];
scope.libraries = [];
scope.sources = [];
function refreshContentIndexes() {
for (let i = 0; i < scope.content.length; i++) {
@ -47,20 +57,173 @@ module.exports = function ($timeout, commonProgramTools, getShowData) {
console.log("movedFunction(" + index + ")");
}
scope.serverChanged = async () => {
if (scope.server === "") {
scope.libraryKey = "";
return;
}
scope.loadingLibraries = true;
try {
let libraries = (await dizquetv.getFromPlexProxy(scope.server, "/library/sections")).Directory;
if ( typeof(libraries) === "undefined") {
libraries = []
}
let officialLibraries = libraries.map( (library) => {
return {
"key" : library.key,
"description" : library.title,
}
} );
let defaultLibrary = {
"key": "",
"description" : "Select a Library...",
}
let playlists = [
{
"key": "$PLAYLISTS",
"description" : "Playlists",
}
];
let combined = officialLibraries.concat(playlists);
if (! combined.some( (library) => library.key === scope.libraryKey) ) {
scope.libraryKey = "";
scope.libraries = [defaultLibrary].concat(combined);
} else {
scope.libraries = combined;
}
} catch (err) {
scope.libraries = [ { name: "", description: "Unable to load libraries"} ];
scope.libraryKey = ""
throw err;
} finally {
scope.loadingLibraries = false;
$timeout( () => {}, 0);
}
}
scope.linker( (filler) => {
scope.libraryChanged = async () => {
if (scope.libraryKey == null) {
throw Error(`null libraryKey? ${scope.libraryKey} ${new Date().getTime()} `);
}
if (scope.libraryKey === "") {
scope.sourceKey = "";
return;
}
scope.loadingCollections = true;
try {
let collections;
if (scope.libraryKey === "$PLAYLISTS") {
collections = (await dizquetv.getFromPlexProxy(scope.server, `/playlists`)).Metadata;
} else {
collections = (await dizquetv.getFromPlexProxy(scope.server, `/library/sections/${scope.libraryKey}/collections`));
collections = collections.Metadata
}
if (typeof(collections) === "undefined") {
//when the library has no collections it returns size=0
//and no array
collections = [];
}
let officialCollections = collections.map( (col) => {
return {
"key" : col.key,
"description" : col.title,
}
} );
let defaultSource = {
"key": "",
"description" : "Select a Source...",
};
if (officialCollections.length == 0) {
defaultSource = {
"key": "",
"description" : "(No collections/lists found)",
}
}
if (! officialCollections.some( (col) => col.key === scope.sourceKey ) ) {
scope.sourceKey = "";
scope.sources = [defaultSource].concat(officialCollections);
} else {
scope.sources = officialCollections;
}
} catch (err) {
scope.sources = [ { name: "", description: "Unable to load collections"} ];
scope.sourceKey = "";
throw err;
} finally {
scope.loadingCollections = false;
$timeout( () => {}, 0);
}
}
let reloadServers = async() => {
scope.loadingServers = true;
try {
let servers = await dizquetv.getPlexServers();
scope.servers = servers.map( (s) => {
return {
"name" : s.name,
"description" : `Plex - ${s.name}`,
}
} );
let defaultServer = {
name: "",
description: "Select a Plex server..."
};
if (! scope.servers.some( (server) => server.name === scope.server) ) {
scope.server = "";
scope.servers = [defaultServer].concat(scope.servers);
}
} catch (err) {
scope.server = "";
scope.servers = [ {name:"", description:"Could not load servers"} ];
throw err;
} finally {
scope.loadingServers = false;
$timeout( () => {}, 0);
}
await scope.serverChanged();
await scope.libraryChanged();
};
scope.linker( async (filler) => {
if ( typeof(filler) === 'undefined') {
scope.name = "";
scope.content = [];
scope.id = undefined;
scope.title = "Create Filler List";
scope.mode = "import";
scope.server = "";
scope.libraryKey = "";
scope.sourceKey = "";
} else {
scope.name = filler.name;
scope.content = filler.content;
scope.id = filler.id;
scope.title = "Edit Filler List";
scope.mode = filler.mode;
scope.server = filler?.import?.serverName;
if ( typeof(scope.server) !== "string" ) {
scope.server = "";
}
scope.libraryKey = filler?.import?.meta?.libraryKey;
if ( typeof(scope.libraryKey) !== "string" ) {
scope.libraryKey = "";
}
scope.sourceKey = filler?.import?.key;
if ( typeof(scope.sourceKey) !== "string" ) {
scope.sourceKey = "";
}
}
await reloadServers();
scope.source = "";
refreshContentIndexes();
scope.visible = true;
} );
@ -73,8 +236,17 @@ module.exports = function ($timeout, commonProgramTools, getShowData) {
if ( (typeof(scope.name) === 'undefined') || (scope.name.length == 0) ) {
scope.error = "Please enter a name";
}
if ( scope.content.length == 0) {
scope.error = "Please add at least one clip.";
if ( scope?.mode === "import" ) {
if ( (typeof(scope?.server) !== "string" ) || (scope?.server === "") ) {
scope.error = "Please select a server"
}
if ( (typeof(scope?.source) !== "string" ) && (scope?.source === "") ) {
scope.error = "Please select a source."
}
} else {
if ( scope.content.length == 0) {
scope.error = "Please add at least one clip.";
}
}
if (typeof(scope.error) !== 'undefined') {
$timeout( () => {
@ -83,14 +255,30 @@ module.exports = function ($timeout, commonProgramTools, getShowData) {
return;
}
scope.visible = false;
scope.onDone( {
let object = {
name: scope.name,
content: scope.content.map( (c) => {
delete c.$index
return c;
} ),
id: scope.id,
} );
mode: scope.mode,
};
if (object.mode === "import") {
object.content = [];
//In reality dizqueTV only needs to know the server name
//and the source key, the meta object is for extra data
//that is useful for external things like this UI.
object.import = {
serverName : scope.server,
key: scope.sourceKey,
meta: {
libraryKey : scope.libraryKey,
}
}
}
scope.onDone( object );
}
scope.getText = (clip) => {
let show = getShowData(clip);

View File

@ -11,8 +11,52 @@
<div class="form-group">
<label for="name">Filler Name:</label>
<input type="text" class="form-control" id="name" placeholder="Filler Name" ng-model="name" ></input>
<div>Mode:</div>
<select class="custom-select"
ng-model="mode"
ng-options="theMode.name as theMode.description for theMode in modes" ></select>
</div>
<div ng-show="mode==='import'">
<div class="form-group">
<label for="server">Server:</label>
<div class="loader" ng-show="loadingServers === true" ></div>
<select
ng-show="loadingServers !== true"
id="server"
class="custom-select"
ng-change="serverChanged()"
ng-model="server"
ng-options="x.name as x.description for x in servers" ></select>
</div>
<div ng-show="server !==''">
<label for="library">Library:</label>
<div class="loader" ng-show="loadingLibraries === true" ></div>
<select
ng-show="loadingLibraries !== true"
id="library"
class="custom-select"
ng-change="libraryChanged()"
ng-model="libraryKey"
ng-options="x.key as x.description for x in libraries" ></select>
</div>
<div ng-show="libraryKey !==''">
<label for="source">Source:</label>
<div class="loader" ng-show="loadingCollections === true" ></div>
<select
ng-show="loadingCollections !== true"
id="source"
class="custom-select"
ng-model="sourceKey"
ng-options="x.key as x.description for x in sources" ></select>
</div>
</div>
<div ng-show="mode==='custom'">
<h6 style="margin-top: 10px;">Clips</h6>
<div class="flex-container">
@ -65,10 +109,11 @@
<div ng-show="content.length === 0">
<p class="text-center text-info">Click the <span class="fa fa-plus"></span> to import filler content from your Plex server(s).</p>
</div>
</div>
</div>
</div>
<div vs-repeat class="modal-body container list-group list-group-root filler-list"
<div ng-show="mode==='custom'" vs-repeat class="modal-body container list-group list-group-root filler-list"
dnd-list="content" ng-if="showList()"
vs-repeat-reinitialized="vsReinitialized(event, startIndex, endIndex)"
ng-init="setUpWatcher()"
@ -76,8 +121,7 @@
dnd-list=""
>
<div class="list-group-item flex-container filler-row" style="cursor: default; height:1.1em; overflow:hidden" ng-repeat="x in content" track-by="x.$index" dnd-draggable="x"
"
<div class="list-group-item flex-container filler-row" style="cursor: default; height:1.1em; overflow:hidden" ng-repeat="x in content" track-by="x.$index" dnd-draggable="x"
dnd-effect-allowed="move"
dnd-moved="movedFunction(x.$index)"
>

View File

@ -3,8 +3,8 @@ module.exports = function ($http, $q) {
getVersion: () => {
return $http.get('/api/version').then((d) => { return d.data })
},
getPlexServers: () => {
return $http.get('/api/plex-servers').then((d) => { return d.data })
getPlexServers: async () => {
return (await $http.get('/api/plex-servers')).data;
},
addPlexServer: (plexServer) => {
return $http({
@ -96,6 +96,9 @@ module.exports = function ($http, $q) {
headers: { 'Content-Type': 'application/json; charset=utf-8' }
}).then((d) => { return d.data })
},
getFFMpegPath: () => {
return $http.get('/api/ffmpeg-info').then((d) => { return d.data })
},
getXmltvSettings: () => {
return $http.get('/api/xmltv-settings').then((d) => { return d.data })
},