Merge pull request #510 from WebTools-NG/#505-FindMedia

#505 find media
This commit is contained in:
Tommy Mikkelsen 2022-06-27 22:14:09 +02:00 committed by GitHub
commit 55931e349e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 877 additions and 133 deletions

View file

@ -13,6 +13,8 @@
* [#498 Add preferences to shows export](https://github.com/WebTools-NG/WebTools-NG/issues/498)
* [#500 Library Path Mapping](https://github.com/WebTools-NG/WebTools-NG/issues/500)
* [#505 FindMedia](https://github.com/WebTools-NG/WebTools-NG/issues/505)
## V0.3.17 (20220601)

32
package-lock.json generated
View file

@ -1,16 +1,16 @@
{
"name": "webtools-ng",
"version": "0.3.17",
"version": "0.3.18",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "webtools-ng",
"version": "0.3.17",
"version": "0.3.18",
"hasInstallScript": true,
"license": "MPL-2.0",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.2",
"@fortawesome/fontawesome-free": "^5.15.4",
"axios": "^0.21.1",
"bootstrap": "^4.5.3",
"bootstrap-vue": "^2.20.1",
@ -4595,14 +4595,20 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001291",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001291.tgz",
"integrity": "sha512-roMV5V0HNGgJ88s42eE70sstqGW/gwFndosYrikHthw98N5tLnOTxFqMLQjZVRxTWFlJ4rn+MsgXrR7MDPY4jA==",
"version": "1.0.30001357",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001357.tgz",
"integrity": "sha512-b+KbWHdHePp+ZpNj+RDHFChZmuN+J5EvuQUlee9jOQIUAdhv9uvAZeEtUeLAknXbkiu1uxjQ9NLp1ie894CuWg==",
"dev": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
}
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
}
]
},
"node_modules/case-sensitive-paths-webpack-plugin": {
"version": "2.4.0",
@ -25155,9 +25161,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30001291",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001291.tgz",
"integrity": "sha512-roMV5V0HNGgJ88s42eE70sstqGW/gwFndosYrikHthw98N5tLnOTxFqMLQjZVRxTWFlJ4rn+MsgXrR7MDPY4jA==",
"version": "1.0.30001357",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001357.tgz",
"integrity": "sha512-b+KbWHdHePp+ZpNj+RDHFChZmuN+J5EvuQUlee9jOQIUAdhv9uvAZeEtUeLAknXbkiu1uxjQ9NLp1ie894CuWg==",
"dev": true
},
"case-sensitive-paths-webpack-plugin": {

View file

@ -24,7 +24,7 @@
},
"main": "background.js",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.2",
"@fortawesome/fontawesome-free": "^5.15.4",
"axios": "^0.21.1",
"bootstrap": "^4.5.3",
"bootstrap-vue": "^2.20.1",

View file

@ -173,7 +173,7 @@
"Name": "ExportTools",
"ErrorServer": "No server",
"Settings": {
"Settings": "Settings",
"Name": "@:Modules.ET.Name Settings",
"Description": "Here you can define the settings for @:Modules.ET.Name",
"ArraySep": "Delimiter for multiple info in same field",
"ColumnSep": "Delimiter between columns in CSV file (Use 9 for TAB)",
@ -460,15 +460,33 @@
"lblBtnCopy": "Copy",
"genRep": "Generate a report"
},
"FindMissing": {
"Name": "Find Missing",
"Description": "@:Modules.PMS.FindMissing.Name module allows you search both filesystem and Plex database to locate medias missing"
"FindMedia": {
"Name": "Find Media",
"Description": "@:Modules.PMS.FindMedia.Name module allows you search both filesystem and Plex database to locate medias missing from either of them",
"RunTask": "Run",
"MissingMapTitle": "Missing a mapped path",
"MissingMapDesc": "One or more defined paths for library is not defined. Goto '@:Modules.PMS.LibMapping.Name' to correct",
"ttSettings": "Settings for @:Modules.PMS.FindMedia.Name module",
"ScanningFS": "Scanning File System",
"ScanningLib": "Scanning Library",
"Settings": {
"Name": "Find Media Settings",
"Description": "@:Modules.PMS.FindMedia.Name settings allows you to customize how @:Modules.PMS.FindMedia.Name works",
"Note": "Note: After changing a setting, please restart @:Common.AppName",
"Ext": "Comma separated file extensions to look for",
"ignoreDirs": "Comma separated list of directories to skip",
"Return": "Save & Return",
"Reset": "Reset to factory standard",
"IgnoreHidden": "Ignore Hidden files/directories on Linux/Mac",
"IgnoreExtras": "Ignore local extras"
}
},
"LibMapping": {
"Name": "Library Mapping",
"Description": "@:Modules.PMS.LibMapping.Name allows you map the defined Library folders path to match a path for this workstation",
"ClickToDefine": "Click to define",
"SelectMapDirPath": "Select mapped path"
"SelectMapDirPath": "Select mapped path",
"TTSelectLibrary": "Here you select the library to map"
}
},
"PlexTV": {

View file

@ -101,22 +101,22 @@
icon: 'fas fa-tv',
},
{
href: { path: '/pms/findmissing' },
title: this.$t("Modules.PMS.FindMissing.Name"),
hidden: wtutils.hideMenu('pmsFindMissing'),
icon: 'fas fa-tv'
href: { path: '/pms/findmedia' },
title: this.$t("Modules.PMS.FindMedia.Name"),
hidden: wtutils.hideMenu('pmsFindMedia'),
icon: 'fas fa-search'
},
{
href: '/pms/libmapping',
hidden: wtutils.hideMenu('pmsLibMapping'),
title: this.$t("Common.Menu.Sidebar.PMS.LibMapping"),
icon: 'fa fa-cog'
icon: 'fas fa-hdd'
},
{
href: '/pms/settings',
hidden: wtutils.hideMenu('pmsSettings'),
title: this.$t("Common.Menu.Sidebar.PMS.Settings"),
icon: 'fa fa-cog'
icon: 'fas fa-cog'
},
{
href: { path: '/pms/viewstate' },
@ -136,13 +136,13 @@
title: this.$t("Common.Menu.Sidebar.Language.NavTitle"),
hidden: wtutils.hideMenu('Language'),
//icon: 'fas fa-language'
icon: 'fa fa-globe'
icon: 'fas fa-globe'
},
{
href: '/settings',
title: this.$t("Common.Menu.Sidebar.Settings.NavTitle"),
hidden: wtutils.hideMenu('Settings'),
icon: 'fa fa-cog'
icon: 'fas fa-cog'
},
{
href: { path: '/about' },

View file

@ -61,7 +61,6 @@
"Suggest Naming": "includeGuids=1&checkFiles=0&includeRelated=0&includeExtras=0&includeBandwidths=0&includeChapters=0&excludeElements=Actor,Collection,Country,Director,Genre,Label,Mood,Producer,Similar,Writer,Role&excludeFields=summary,tagline",
"all": "checkFiles=1&includeExtras=1&includeBandwidths=1&includeChapters=1"
}
},
"2": {
"levels": {

View file

@ -0,0 +1,48 @@
/*
This file contains different functions and methods
that we use fo PMS.
*/
const log = require('electron-log');
console.log = log.log;
const {JSONPath} = require('jsonpath-plus');
import store from '../../../store';
const pms = new class PMS {
constructor() {
}
async getPMSSections( libTypes ){
/*
This function will return an array of libraries of specified types
Param: libTypes Like: ['movie', 'show']
*/
await store.dispatch('fetchSections');
const sections = await store.getters.getPmsSections;
const result = [];
this.selLib = "";
if (Array.isArray(sections) && sections.length) {
sections.forEach(req => {
if ( libTypes.includes(req.type)){
log.debug(`[pms.js] (getPMSSections) - pushing library: ${req.title} to results`);
let item = [];
let itemVal = {};
itemVal['key'] = JSONPath({path: '$..key', json: req})[0];
itemVal['location'] = JSONPath({path: '$..path', json: req.location});
itemVal['type'] = JSONPath({path: '$..type', json: req})[0];
item['text']=req.title;
//item['value']=JSONPath({path: '$..path', json: req.location});
item['value']=itemVal;
result.push(Object.assign({}, item));
}
});
} else {
log.error("[pms.js] (getPMSSections) - No Library found");
}
log.debug(`[pms.js] (getPMSSections) - reslut: ${JSON.stringify(result)}`);
return result;
}
}
export { pms };

View file

@ -235,6 +235,12 @@ const wtutils = new class WTUtils {
}
}
async sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
hideMenu(menu)
{
let retVal = false;
@ -289,11 +295,6 @@ const wtutils = new class WTUtils {
if ( wtconfig.get('Menu.About', 'N/A') == 'N/A' ){
wtconfig.set('Menu.About', false)
}
// General section
if ( wtconfig.get('General.username', 'N/A') == 'N/A' ){
wtconfig.set('General.username', '')
@ -351,6 +352,19 @@ const wtutils = new class WTUtils {
if ( wtconfig.get('PMS.ContainerSize.3001', 'N/A') == 'N/A' ){
wtconfig.set('PMS.ContainerSize.3001', 20)
}
if ( wtconfig.get('PMS.FindMedia.Settings.Ext', 'N/A') == 'N/A' ){
wtconfig.set('PMS.FindMedia.Settings.Ext', [
"3g2","3gp","asf","asx","avc",
"avi","avs","bivx","bup","divx",
"dv","dvr-ms","evo","fli","flv",
"m2t","m2ts","m2v","m4v","mkv",
"mov","mp4","mpeg","mpg","mts",
"nsv","nuv","ogm","ogv","tp",
"pva","qt","rm","rmvb","sdp",
"svq3","strm","ts","ty","vdr",
"viv","vob","vp3","wmv","wpl",
"wtv","xsp","xvid","webm"])
}
// ET Settings
if ( wtconfig.get('ET.ChReturn', 'N/A') == 'N/A' ){
wtconfig.set('ET.ChReturn', '<RETURN>')

View file

@ -0,0 +1,162 @@
<template>
<div>
<!-- Settings button -->
<div class="text-right">
<div class="buttons">
<!-- Buttons -->
<div id="buttons">
<b-button-group id="settings">
<b-tooltip target="settings" triggers="hover">
{{ $t('Modules.PMS.FindMedia.ttSettings') }}
</b-tooltip>
<button class="btn btn-outline-success" @click="showSettings"><i class="fa fa-cog"></i></button>
</b-button-group>
</div>
</div>
</div>
<!-- Main view -->
<b-container fluid>
<div class="col-lg-10 col-md-12 col-xs-12">
<h1>{{ $t("Modules.PMS.FindMedia.Name") }}</h1>
<p>{{ $t("Modules.PMS.FindMedia.Description") }}</p>
</div>
<!-- Select Lib -->
<div class="d-flex align-items-center">
<b-form-group id="SelLibGroup" v-bind:label="$t('Modules.ET.optExpType.lblSelectSelection')" label-size="lg" label-class="font-weight-bold pt-0">
<b-tooltip target="SelLibGroup" triggers="hover">
{{ $t('Modules.PMS.LibMapping.TTSelectLibrary') }}
</b-tooltip>
<b-form-select
v-model="selLib"
id="selLib"
:options="selLibOptions"
name="selLib">
</b-form-select>
</b-form-group>
</div>
<br>
<!-- Buttons -->
<div class="buttons">
<!-- Buttons -->
<div id="buttons" class="text-center">
<b-button-group >
<b-button variant="success" class="mr-1" :disabled="this.selLib == ''" @click="runFM"> {{ $t('Modules.PMS.FindMedia.RunTask') }} </b-button>
</b-button-group>
</div>
</div>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<statusDiv /> <!-- Status Div -->
</b-container>
</div>
</template>
<script>
import i18n from '../../../../i18n';
//import store from '../../../store';
import { dialog } from '../../General/wtutils';
import { pms } from '../../General/pms';
import { findMedia } from './scripts/FindMedia.js';
import statusDiv from '../../General/status.vue';
import { status } from '../../General/status';
i18n
const log = require("electron-log");
export default {
components: {
statusDiv
},
data() {
return {
serverIsSelected: false,
selLibOptions: [],
selLib: "",
selLibraryWait: true
};
},
created() {
log.info("FindMedia Created");
this.serverSelected();
this.getPMSSections();
},
watch: {
// Watch for when selected server address is updated
selectedServerAddress: async function(){
// Changed, so we need to update the libraries
this.selLibraryWait = true;
await this.getPMSSections();
this.selLibraryWait = true;
}
},
computed: {
// We need this computed, for watching for changes to selected server
selectedServerAddress: function(){
return this.$store.getters.getSelectedServerAddress
}
},
methods: {
// Show Settings
showSettings(){
this.$router.push({ name: 'FindMediaSettings' })
},
// Run FindMedia
async runFM() {
log.info(`[FindMedia.vue] (runFM) starting`);
// Check if we have all lib paths mapped
const mappedPathOK = await findMedia.checkPathMapping( this.selLib["location"] );
if ( mappedPathOK === 'WTNG_ERROR_WTNG')
{
log.error(`[FindMedia.vue] (runFM) - Missing mapped path for: ${mappedPathOK}`);
dialog.ShowMsgBox(i18n.t("Modules.PMS.FindMedia.MissingMapDesc"), 'error', i18n.t("Modules.PMS.FindMedia.MissingMapTitle"), [i18n.t("Common.Ok")]);
}
else{
log.info(`[FindMedia.vue] (runFM) mappedPath is okay`);
await status.clearStatus();
await status.updateStatusMsg( status.RevMsgType.Status, i18n.t('Common.Status.Msg.Processing'));
// Wait a short moment, so status can update
await new Promise(resolve => setTimeout(resolve, 50));
await findMedia.findMedia( this.selLib["location"], this.selLib["key"], this.selLib["type"] );
}
},
// Get Library list
getPMSSections: async function(){
this.selLibrary = "";
this.selLibOptions = await pms.getPMSSections(['movie', 'show']);
},
/* Check if a server is selected, and if not
tell user, and disable backup */
async serverSelected() {
let serverCheck = this.$store.getters.getSelectedServer;
this.serverIsSelected = ( this.$store.getters.getSelectedServer != "none" );
if (serverCheck == "none") {
log.debug("serverCheck is none");
this.selDVR = "";
this.$bvToast.toast(this.$t("Modules.PMS.ErrorNoServerSelectedMsg"), {
title: this.$t("Modules.PMS.ErrorNoServerSelectedTitle"),
autoHideDelay: 4000,
solid: true,
variant: 'primary',
toaster: 'b-toaster-bottom-right'
});
}
}
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
#sync-button {
margin-left: 1em;
}
</style>

View file

@ -0,0 +1,145 @@
<template>
<b-container fluid>
<div class="col-lg-10 col-md-12 col-xs-12">
<br>
<br>
<h1>{{ $t("Modules.PMS.FindMedia.Settings.Name") }}</h1>
<p>{{ $t("Modules.PMS.FindMedia.Settings.Description") }}</p>
<br>
<p>{{ $t("Modules.PMS.FindMedia.Settings.Note") }}</p>
</div>
<b-input-group id="ExtGrp" :prepend="$t('Modules.PMS.FindMedia.Settings.Ext')" class="mt-3">
<b-form-textarea
id="Ext" name="Ext" type="text" class="form-control" v-model="Ext" @change="setExt"
rows="3"
max-rows="3"
>
</b-form-textarea>
</b-input-group>
<b-input-group id="ignoreDirsGrp" :prepend="$t('Modules.PMS.FindMedia.Settings.ignoreDirs')" class="mt-3">
<b-form-textarea
id="ignoreDirs" name="ignoreDirs" type="text" class="form-control" v-model="ignoreDirs" @change="setIgnoreDirs"
rows="3"
max-rows="3"
>
</b-form-textarea>
</b-input-group>
<br>
<div>
<b-form-checkbox
id="IgnoreHidden"
v-model="IgnoreHidden"
name="IgnoreHidden"
value=true
unchecked-value=false
@change="setIgnoreHidden"
>
{{ $t('Modules.PMS.FindMedia.Settings.IgnoreHidden') }}
</b-form-checkbox>
</div>
<div>
<b-form-checkbox
id="IgnoreExtras"
v-model="IgnoreExtras"
name="IgnoreExtras"
value=true
unchecked-value=false
@change="setIgnoreExtras"
>
{{ $t('Modules.PMS.FindMedia.Settings.IgnoreExtras') }}
</b-form-checkbox>
</div>
<br>
<br>
<!-- Buttons -->
<div class="buttons">
<!-- Buttons -->
<div id="buttons" class="text-center">
<b-button-group >
<b-button variant="danger" class="mr-1" @click="reset"> {{ $t('Modules.PMS.FindMedia.Settings.Reset') }} </b-button>
<b-button variant="success" class="mr-1" @click="jumpToFM"> {{ $t('Modules.PMS.FindMedia.Settings.Return') }} </b-button>
</b-button-group>
</div>
</div>
</b-container>
</template>
<script>
import { wtconfig } from '../../../General/wtutils';
import { findMedia } from '../scripts/FindMedia';
const log = require("electron-log");
export default {
data() {
return {
Ext: "",
IgnoreHidden: true,
IgnoreExtras: true,
ignoreDirs: ""
}
},
created() {
log.info(`[FindMediaSettings.vue] (Created) - FMSettings created`);
this.getSettings();
},
methods: {
setIgnoreHidden( value ){
wtconfig.set('PMS.FindMedia.Settings.IgnoreHidden', value);
},
setIgnoreExtras( value ){
wtconfig.set('PMS.FindMedia.Settings.IgnoreExtras', value);
},
getSettings(){
this.Ext = wtconfig.get('PMS.FindMedia.Settings.Ext').toString();
this.IgnoreHidden = wtconfig.get('PMS.FindMedia.Settings.IgnoreHidden', true);
this.IgnoreExtras = wtconfig.get('PMS.FindMedia.Settings.IgnoreExtras', true);
this.ignoreDirs = wtconfig.get('PMS.FindMedia.Settings.ignoreDirs', findMedia.ignoreDirs).toString();
},
setExt( value ){
// Remove spaces, and force to lower case
let ext = value.trimStart().trimEnd().toLowerCase();
// Update form
this.Ext = ext;
// Update conf file
wtconfig.set('PMS.FindMedia.Settings.Ext', ext.split(','));
},
setIgnoreDirs( value ){
let ignoreDirsArr = value.split(',');
let newIgnoreDirsArr = [];
ignoreDirsArr.forEach(element => {
newIgnoreDirsArr.push(element.trimStart().trimEnd().toLowerCase());
});
// Update form
this.ignoreDirs = newIgnoreDirsArr.toString();
// Update conf file
wtconfig.set('PMS.FindMedia.Settings.ignoreDirs', newIgnoreDirsArr);
},
// Return to FindMedia
jumpToFM(){
this.$router.push({ name: 'FindMedia' })
},
// Reset to factory Std
reset(){
this.Ext = findMedia.validExt.toString();
wtconfig.set('PMS.FindMedia.Settings.Ext', findMedia.validExt);
this.ignoreDirs = findMedia.ignoreDirs.toString();
wtconfig.set('PMS.FindMedia.Settings.ignoreDirs', findMedia.ignoreDirs);
this.IgnoreHidden = true;
wtconfig.set('PMS.FindMedia.Settings.IgnoreHidden', true);
wtconfig.set('PMS.FindMedia.Settings.IgnoreExtras', true);
this.IgnoreExtras = true;
log.info(`[FindMediaSettings.vue] (reset) - FMSettings factory reset done`);
}
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
#sync-button {
margin-left: 1em;
}
</style>

View file

@ -0,0 +1,413 @@
// This file holds generic FM functions
const log = require('electron-log');
console.log = log.log;
const fs = require("fs");
const path = require("path");
const {JSONPath} = require('jsonpath-plus');
var sanitize = require("sanitize-filename");
import store from '../../../../../store';
import { wtutils,wtconfig } from '../../../General/wtutils';
import { status } from '../../../General/status';
import i18n from '../../../../../i18n';
import { resolve } from 'path';
import axios from 'axios';
import {csv} from '../../../ExportTools/scripts/csv';
const validDir = function( dirName ) {
/* Will check if directory is not hidden or should be ignored
returns true or false */
log.silly(`Checking if ${dirName} is valid`);
// Got a hidden one?
if ( wtconfig.get('PMS.FindMedia.Settings.IgnoreHidden', true) === 'true' ){
if ( dirName.startsWith('.') ){
log.silly(`We do not allow hidden dirs like: ${dirName}`);
return false;
}
}
// Got an Extra dir?
if ( findMedia.settingsIgnoreExtras ){
if ( findMedia.ExtraDirs.includes( dirName )){
log.silly(`We do not allow extra dirs like: ${dirName}`);
return false;
}
}
// Got a dir to ignore?
if ( findMedia.settingsIgnoreDirs.includes( dirName ) ){
log.silly(`We do not allow ignore dirs like: ${dirName}`);
return false;
}
log.silly(`${dirName} is valid`);
return true;
}
const validFile = function( fileName ) {
/* Will check if file is valid or not
returns true or false */
log.silly(`[FindMedia.js] (validFile) - Checking file: ${fileName}`)
if ( findMedia.validExt.includes(path.extname(fileName).toLowerCase().slice(1))){
log.silly(`[FindMedia.js] (validFile) - Valid ext for file: ${fileName}`);
if ( findMedia.settingsIgnoreExtras ){
log.silly(`[FindMedia.js] (validFile) - Checking IgnoreExtras for file: ${fileName}`)
for (let eFile of findMedia.Extrafiles) {
if ( path.parse(fileName).name.endsWith(eFile) ){
log.silly(`We ignore extra file: ${fileName}`)
return false;
}
}
return true;
} else { return true }
} else {
log.silly(`[FindMedia.js] (validFile) - Ext not valid for file: ${fileName}`)
return false
}
}
const getAllFiles = function( dirPath, orgDirPath, arrayOfFiles ) {
/*
Recursive scanning of a filepath
Takes dirPath and orgDirPath as parameter
for the starting dir. (Should be the same)
Will return an raw array, as well as populate
findMedia.filesFound array
*/
var files = fs.readdirSync(dirPath);
// Set array if needed
arrayOfFiles = arrayOfFiles || [];
files.forEach(function(curFile) {
// Is this a directory?
if (fs.statSync(dirPath + "/" + curFile).isDirectory()) {
// Check if valid dir, then call req.
if ( validDir( curFile ) ){
arrayOfFiles = getAllFiles(path.join(dirPath, curFile), orgDirPath, arrayOfFiles)
}
} else {
// We found a file
if ( validFile( curFile ) ){
// Force forward slash
let lookupPath = path.join(dirPath, curFile).replaceAll('\\', '/');
log.silly(`[FindMedia.js] (getAllFiles) - Adding ${lookupPath.slice(orgDirPath.length + 1)}: ${ path.join(dirPath, curFile) }`);
//status.updateStatusMsg( status.RevMsgType.Item, `GED Found file: ${curFile}`);
findMedia.filesFound[lookupPath.slice(orgDirPath.length + 1)] = path.join(dirPath, curFile);
}
}
})
return arrayOfFiles
}
const findMedia = new class FINDMEDIA {
constructor() {
this.validExt = [
'3g2','3gp','asf','asx','avc','avi','avs','bivx','bup','divx','dv','dvr-ms',
'evo','fli','flv','m2t','m2ts','m2v','m4v',
'mkv','mov','mp4','mpeg','mpg','mts','nsv','nuv','ogm','ogv','tp','pva','qt','rm','rmvb','sdp','svq3',
'strm','ts','ty','vdr','viv','vob','vp3','wmv','wpl','wtv','xsp','xvid','webm'
];
this.ExtraDirs = [
'Behind The Scenes', 'Deleted Scenes', 'Featurettes',
'Interviews', 'Scenes', 'Shorts', 'Trailers', 'Other'
];
this.Extrafiles = [
'-behindthescenes', '-deleted', '-featurette',
'-interview', '-scene', '-short', '-trailer', '-other'
];
this.ignoreDirs = [
'lost+found'
];
this.libPaths = [];
this.filePath = [];
this.validExt = [];
this.currentLibPathLength;
this.filesFound = {};
this.libFiles = [];
this.PMSLibPaths = [];
this.csvFile = '';
this.libName = '';
this.csvStream;
this.settingsIgnoreExtras;
this.ignoreDirs;
}
// Generate the filename for an export
async getFileName({ Type, libKey }){
const dateFormat = require('dateformat');
const OutDir = wtconfig.get('General.ExportPath');
const timeStamp = dateFormat(new Date(), "yyyy.mm.dd_h.MM.ss");
const path = require('path');
let arrFile = [];
arrFile.push(store.getters.getSelectedServer.name); //PMSName
arrFile.push(JSONPath({path: `$.[?(@.key==${libKey})].title`, json: store.getters.getPmsSections})); //libName
arrFile.push(timeStamp + '.' + Type + '.tmp');
let outFile = sanitize(arrFile.join('_'));
const targetDir = path.join(OutDir, wtutils.AppName, i18n.t('Modules.PMS.Name'), i18n.t('Modules.PMS.FindMedia.Name'));
const outFileWithPath = path.join(targetDir, outFile);
// Make sure target dir exists
const fs = require('fs')
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
log.info(`[FindMedia.js] (getFileName) - OutFile is: ${outFileWithPath}`);
return outFileWithPath;
}
async makeOutFile( libKey ){
let csvFile;
const intSep = '{*WTNG-ET*}';
// Get Header fields
let fields = ['Title', 'PMS file', 'Filesystem'];
var fs = require('fs');
csvFile = await this.getFileName({ Type: 'csv', libKey: libKey });
this.csvStream = fs.createWriteStream(csvFile, {flags:'a'});
await csv.addHeaderToTmp({ stream: this.csvStream, item: fields});
const NA = wtconfig.get('ET.NotAvail', 'N/A');
// Add only on File System
for(var key in this.filesFound){
//console.log(key+": "+this.filesFound[key]);
let row = NA + intSep + NA + intSep + this.filesFound[key];
row = row.replaceAll(intSep, wtconfig.get("ET.ColumnSep", '|'));
csv.addRowToTmp({ stream: this.csvStream, item: row});
}
// Add only in PMS
for( key in this.libFiles){
console.log( JSON.stringify(this.libFiles[key]));
let row = this.libFiles[key]['title'] + intSep + this.libFiles[key]['file'] + intSep + NA;
row = row.replaceAll(intSep, wtconfig.get("ET.ColumnSep", '|'));
csv.addRowToTmp({ stream: this.csvStream, item: row});
}
// Close filestream
this.csvStream.end();
// Rename outFile to real name
let newFile;
newFile = csvFile.replace('.tmp', '')
fs.renameSync(csvFile, newFile);
return newFile;
}
async findMedia( libpaths, libKey, libType ){
status.updateStatusMsg( status.RevMsgType.Status, i18n.t('Common.Status.Msg.Processing'));
// Get settings needed
this.validExt = await wtconfig.get('PMS.FindMedia.Settings.Ext');
this.settingsIgnoreExtras = await wtconfig.get('PMS.FindMedia.Settings.IgnoreExtras');
this.settingsIgnoreDirs = await wtconfig.get('PMS.FindMedia.Settings.ignoreDirs');
// Scan file system
this.filesFound = [];
await findMedia.scanFileSystemPaths( libpaths );
// Scan library
await findMedia.scanPMSLibrary(libKey, libType);
// Create output file
let outFile = await this.makeOutFile( libKey );
status.clearStatus();
status.updateStatusMsg( status.RevMsgType.Status, i18n.t('Common.Status.Msg.Finished'));
status.updateStatusMsg( status.RevMsgType.OutFile, outFile);
libType
}
async checkPathMapping( paths ){
/*
This will check paths if mapped
will return 'ok' if all is good, or
else the path that needs a mapping
*/
return new Promise((resolve) => {
let gotError = false;
log.info(`[FindMedia.js] (checkPathMapping) Starting`);
log.verbose(`[FindMedia.js] (checkPathMapping) We will check the following filePaths: ${ JSON.stringify(paths)}`);
// First we need to check if libPath Mappings has been defined, and if not, alert user
// To do this, we need the selected servers ID
// Get ServerID
let retVal = 'ok'
const serverID = store.getters.getSelectedServer.clientIdentifier;
paths.forEach(libPath => {
// Escape . in libPath
libPath = libPath.replace('.', '\\.');
let mappedLibPath = wtconfig.get(`PMS.LibMapping.${serverID}.${libPath}`, 'WTNG_ERROR_WTNG');
if ( mappedLibPath === 'WTNG_ERROR_WTNG'){
log.error(`[FindMedia.js] (checkPathMapping) - missing: ${libPath}`);
gotError = true;
}
});
log.info(`[FindMedia.js] (checkPathMapping) All done`);
if ( gotError){
retVal = 'WTNG_ERROR_WTNG';
}
resolve (retVal);
});
}
async scanFileSystemPaths( paths ){
/// This will scan the filesystem for medias
status.updateStatusMsg( status.RevMsgType.Info, i18n.t('Modules.PMS.FindMedia.ScanningFS'));
await new Promise(resolve => setTimeout(resolve, 50));
return new Promise((resolve) => {
log.info(`[FindMedia.js] (scanFileSystemPaths) - Starting`);
log.debug(`[FindMedia.js] (scanFileSystemPaths) - We will scan the following filePaths: ${ JSON.stringify(paths)}`);
// Reset output
findMedia.filePath = [];
this.filesFound = {};
const serverID = store.getters.getSelectedServer.clientIdentifier;
//Walk each paths
paths.forEach(async libPath => {
let mappedLibPath = wtconfig.get(`PMS.LibMapping.${serverID}.${libPath.replace('.', '\\.')}`, 'WTNG_ERROR_WTNG');
status.updateStatusMsg( status.RevMsgType.Info, `Now Scanning ${mappedLibPath}`);
log.debug(`[FindMedia.js] (scanFileSystemPaths) - PMS path is: ${libPath}`);
log.debug(`[FindMedia.js] (scanFileSystemPaths) - Wkstn path is: ${mappedLibPath}`);
//findMedia.filePath.push(...getAllFiles( mappedLibPath, mappedLibPath ));
findMedia.filePath.push(...getAllFiles( mappedLibPath, mappedLibPath ));
});
log.info(`[FindMedia.js] (scanFileSystemPaths) - End`);
resolve();
});
}
async scanPMSLibrary( library, libType ){
/*
This will scan the PMS library, and add result to this.libPaths,
if not present in this.filePath.
If present, then pop it from this.filePath
*/
log.info(`[FindMedia.js] (scanPMSLibrary) - Starting`);
log.verbose(`[FindMedia.js] (scanPMSLibrary) - We will scan library with a key of: ${ library } and a type of: ${libType}`);
status.updateStatusMsg( status.RevMsgType.Info, i18n.t('Modules.PMS.FindMedia.ScanningLib'));
await findMedia.getPMSPathArr();
findMedia.libFiles = [];
// We need to find type of lib, and total count as well
let index = 0;
let step = 0;
let size = 0;
let totalSize = 0;
let mediaType = 1;
if ( libType === 'show'){
mediaType = 4;
}
let url = `${store.getters.getSelectedServerAddress}/library/sections/${library}/all?excludeElements=Genre,Director,Writer,Country,Role,Producer,Collections&excludeFields=summary,tagline,rating,contentRating,audienceRatingImage&X-Plex-Container-Start=${index}&X-Plex-Container-Size=${step}&type=${mediaType}`
let header = wtutils.PMSHeader;
header['X-Plex-Token'] = store.getters.getSelectedServer.accessToken;
await axios({
method: 'get',
url: url,
headers: header
})
.then((response) => {
log.debug('[FindMedia.js] (scanPMSLibrary) - Response recieved');
log.silly(`[FindMedia.js] (scanPMSLibrary) - Response returned as: ${JSON.stringify(response.data)}`);
totalSize = JSONPath({path: `$.MediaContainer.totalSize`, json: response.data})[0];
if ( JSONPath({path: `$.MediaContainer.viewGroup`, json: response.data})[0] === 'show'){
mediaType = 4;
}
})
.catch(function (error) {
if (error.response) {
log.error(`[FindMedia.js] (scanPMSLibrary) - ${JSON.stringify(error.response.data)}`);
alert(error.response.data.errors[0].code + " " + error.response.data.errors[0].message);
} else if (error.request) {
log.error(`[FindMedia.js] (scanPMSLibrary) - ${JSON.stringify(error.request)}`);
} else {
log.error(`[FindMedia.js] (scanPMSLibrary) - ${JSON.stringify(error.message)}`);
}
});
// totalSize, size
// resolve();
step = wtconfig.get("PMS.ContainerSize." + mediaType, 20);
let metaData;
do {
url = `${store.getters.getSelectedServerAddress}/library/sections/${library}/all?excludeElements=Genre,Director,Writer,Country,Role,Producer,Collections&excludeFields=summary,tagline,rating,contentRating,audienceRatingImage&X-Plex-Container-Start=${index}&X-Plex-Container-Size=${step}&type=${mediaType}`;
status.updateStatusMsg( status.RevMsgType.Items, i18n.t('Common.Status.Msg.ProcessItem_0_1', {count: index, total: totalSize}));
log.verbose(`[FindMedia.js] (scanPMSLibrary) - Calling url: ${ url } `);
await axios({
method: 'get',
url: url,
headers: header
})
.then((response) => {
log.debug('[FindMedia.js] (scanPMSLibrary) - Response recieved');
log.silly(`[FindMedia.js] (scanPMSLibrary) - Response returned as: ${JSON.stringify(response.data)}`);
size = JSONPath({path: `$.MediaContainer.size`, json: response.data})[0];
metaData = JSONPath({path: `$.MediaContainer.Metadata`, json: response.data})[0];
for (var idxMetaData in metaData)
{
var title = JSONPath({path: `$..title`, json: metaData[parseInt(idxMetaData)]})[0];
var files = JSONPath({path: `$..Part[*].file`, json: metaData[parseInt(idxMetaData)]});
for (var idxFiles in files){
if (this.validExt.includes(path.extname(files[idxFiles]).toLowerCase().slice(1))){
for (var idxPMSLibPaths in this.PMSLibPaths){
if (files[idxFiles].startsWith(this.PMSLibPaths[idxPMSLibPaths])){
// Slice to lookup in files found
var lookup = files[idxFiles].slice(this.PMSLibPaths[idxPMSLibPaths].length + 1)
lookup = lookup.replaceAll('\\', '/');
if ( Object.prototype.hasOwnProperty.call(this.filesFound, lookup)) {
// We need to remove from detected files, since we found it
delete this.filesFound[lookup];
}
else {
// Not found, so only in PMS
let entry = {}
entry['title'] = title;
entry['file'] = files[idxFiles]
this.libFiles.push(entry);
}
}
}
}
}
}
index += step;
})
.catch(function (error) {
if (error.response) {
log.error(`[FindMedia.js] (scanPMSLibrary) - ${JSON.stringify(error.response.data)}`);
alert(error.response.data.errors[0].code + " " + error.response.data.errors[0].message);
return
} else if (error.request) {
log.error(`[FindMedia.js] (scanPMSLibrary) - ${JSON.stringify(error.request)}`);
return
} else {
log.error(`[FindMedia.js] (scanPMSLibrary) - ${JSON.stringify(error.message)}`);
return
}
});
} while ( size == step );
log.info(`[FindMedia.js] (scanPMSLibrary) - End`);
resolve();
}
async getPMSPathArr(){
/*
This will populate this.PMSLibPaths
with an array of library paths used
*/
log.info(`[FindMedia.js] (getPMSPathArr) - Start`);
// Reset property
this.PMSLibPaths = [];
// Start by getting Server ID
const serverID = store.getters.getSelectedServer.clientIdentifier;
// Now lookup defined mappings, so we can add them to the array
let mappedLibPaths = wtconfig.get(`PMS.LibMapping.${serverID}`, 'WTNG_ERROR_WTNG');
for (var idxPath in Object.keys(mappedLibPaths)){
this.PMSLibPaths.push(Object.keys(mappedLibPaths)[idxPath]);
}
log.debug(`[FindMedia.js] (getPMSPathArr) - PMSLibPaths is ${this.PMSLibPaths}`);
log.info(`[FindMedia.js] (getPMSPathArr) - End`);
resolve();
}
}
export { findMedia };

View file

@ -1,62 +0,0 @@
<template>
<b-container fluid>
<div class="col-lg-10 col-md-12 col-xs-12">
<h1>{{ $t("Modules.PMS.FindMissing.Name") }}</h1>
<p>{{ $t("Modules.PMS.FindMissing.Description") }}</p>
</div>
</b-container>
</template>
<script>
import i18n from '../../../../i18n';
//import store from '../../../store';
//import { wtconfig } from '../General/wtutils';
//import { dvr } from "./scripts/dvr";
i18n
const log = require("electron-log");
export default {
data() {
return {
serverIsSelected: false
};
},
created() {
log.info("FindMissing Created");
this.serverSelected();
},
computed: {
// We need this computed, for watching for changes to selected server
selectedServerAddress: function(){
return this.$store.getters.getSelectedServerAddress
}
},
methods: {
/* Check if a server is selected, and if not
tell user, and disable backup */
async serverSelected() {
let serverCheck = this.$store.getters.getSelectedServer;
this.serverIsSelected = ( this.$store.getters.getSelectedServer != "none" );
if (serverCheck == "none") {
log.debug("serverCheck is none");
this.selDVR = "";
this.$bvToast.toast(this.$t("Modules.PMS.ErrorNoServerSelectedMsg"), {
title: this.$t("Modules.PMS.ErrorNoServerSelectedTitle"),
autoHideDelay: 4000,
solid: true,
variant: 'primary',
toaster: 'b-toaster-bottom-right'
});
}
}
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
#sync-button {
margin-left: 1em;
}
</style>

View file

@ -6,10 +6,11 @@
</div>
<br>
<br>
<!-- Select Lib -->
<div class="d-flex align-items-center">
<b-form-group id="SelLibGroup" v-bind:label="$t('Modules.ET.optExpType.lblSelectSelection')" label-size="lg" label-class="font-weight-bold pt-0">
<b-tooltip target="SelLibGroup" triggers="hover">
{{ $t('Modules.PMS.Butler.TTSelectTask') }}
{{ $t('Modules.PMS.LibMapping.TTSelectLibrary') }}
</b-tooltip>
<b-form-select
v-model="selLib"
@ -38,10 +39,7 @@
import i18n from '../../../../i18n';
import store from '../../../../store';
import { wtconfig, dialog } from '../../General/wtutils';
//import { dvr } from "./scripts/dvr";
const {JSONPath} = require('jsonpath-plus');
i18n
import { pms } from '../../General/pms';
const log = require("electron-log");
export default {
@ -57,6 +55,7 @@
created() {
log.info("LibraryMapping Created");
this.serverSelected();
this.getPMSSections();
},
watch: {
// Watch for when selected server address is updated
@ -83,38 +82,25 @@
{
let curVal = {};
curVal['PMS'] = this.items[index]['PMS'];
// If a dot is present in the path, we need to escape it
curVal['PMS'] = curVal['PMS'].replace('.', '\\.');
curVal['Workstation'] = mapDir[0];
curVal['_rowVariant'] = "success";
this.items[index] = curVal;
// Update view
this.$refs.table.refresh();
// Get ServerID
const serverID = store.getters.getSelectedServer.clientIdentifier;
// Save setting
wtconfig.set(`PMS.LibMapping.${serverID}.${curVal['PMS']}`, curVal['Workstation']);
// Remove escape char for viewing
curVal['PMS'] = curVal['PMS'].replace('\\.', '.');
this.items[index] = curVal;
// Update view
this.$refs.table.refresh();
}
},
getPMSSections: async function(){
this.selLibrary = "";
await this.$store.dispatch('fetchSections')
const sections = await this.$store.getters.getPmsSections;
const result = [];
this.selLib = "";
if (Array.isArray(sections) && sections.length) {
sections.forEach(req => {
if ( ['movie', 'show'].includes(req.type)){
log.debug(`[LibraryMapping.vue] (getPMSSections) - pushing library: ${req.title} to results`);
let item = [];
item['text']=req.title;
item['value']=JSONPath({path: '$..path', json: req.location});
result.push(Object.assign({}, item));
}
});
} else {
log.error("[LibraryMapping.vue] (getPMSSections) - No Library found");
result.push["No Library found"];
}
this.selLibOptions = result;
this.selLibOptions = await pms.getPMSSections(['movie', 'show']);
},
/* Check if a server is selected, and if not
tell user, and disable backup */
@ -138,24 +124,30 @@
const serverID = store.getters.getSelectedServer.clientIdentifier;
log.info(`[LibraryMapping.vue] (getLibPath) ServerID is: ${serverID}`);
const fs = require("fs");
arguments[0].forEach(element => {
let entry;
const wkstnPath = wtconfig.get(`PMS.LibMapping.${serverID}.${element}`, this.$t("Modules.PMS.LibMapping.ClickToDefine"));
arguments[0]['location'].forEach(element => {
let entry, virtualElement;
virtualElement = element.replace('.', '\\.');
//const wkstnPath = wtconfig.get(`PMS.LibMapping.${serverID}.${element}`, this.$t("Modules.PMS.LibMapping.ClickToDefine"));
const wkstnPath = wtconfig.get(`PMS.LibMapping.${serverID}.${virtualElement}`, this.$t("Modules.PMS.LibMapping.ClickToDefine"));
log.info(`[LibraryMapping.vue] (getLibPath) Saved path is: ${wkstnPath}`);
// Check if path exists
if (fs.existsSync(wkstnPath)) {
log.debug(`[LibraryMapping.vue] (getLibPath) Saved path existed`);
//element = element.replace('\\.', '.')
entry = {PMS: element, Workstation: wkstnPath, _rowVariant: 'success'};
//entry = {PMS: virtualElement, Workstation: wkstnPath, _rowVariant: 'success'};
} else {
log.debug(`[LibraryMapping.vue] (getLibPath) Saved path not defined`);
if (fs.existsSync(element)) {
log.debug(`[LibraryMapping.vue] (getLibPath) PMS path existed`);
wtconfig.set(`PMS.LibMapping.${serverID}.${element}`, element);
entry = {PMS: element, Workstation: element, _rowVariant: 'success'};
//entry = {PMS: virtualElement, Workstation: element, _rowVariant: 'success'};
}
else {
log.error(`[LibraryMapping.vue] (getLibPath) PMS path unknown`);
entry = {PMS: element, Workstation: wkstnPath, _rowVariant: 'danger'};
//entry = {PMS: virtualElement, Workstation: wkstnPath, _rowVariant: 'danger'};
}
}
arrPath.push(entry);

View file

@ -21,10 +21,10 @@
<br />
* {{ $t("Modules.PMS.DVR.Description") }}</p>
</div>
<div v-if="showFindMissing">
<p><b>{{ $t("Modules.PMS.FindMissing.Name") }}</b>
<div v-if="showFindMedia">
<p><b>{{ $t("Modules.PMS.FindMedia.Name") }}</b>
<br />
* {{ $t("Modules.PMS.FindMissing.Description") }}</p>
* {{ $t("Modules.PMS.FindMedia.Description") }}</p>
</div>
<div v-if="showLibMapping">
<p><b>{{ $t("Modules.PMS.LibMapping.Name") }}</b>
@ -64,7 +64,7 @@
selLevel: "",
showButler: !(wtutils.hideMenu('pmsButler')),
showDVR: !(wtutils.hideMenu('pmsDVR')),
showFindMissing: !(wtutils.hideMenu('pmsFindMissing')),
showFindMedia: !(wtutils.hideMenu('pmsFindMedia')),
showLibMapping: !(wtutils.hideMenu('pmsLibMapping')),
showSettings: !(wtutils.hideMenu('pmsSettings')),
showViewState: !(wtutils.hideMenu('pmsViewState'))

View file

@ -12,7 +12,8 @@ import Butler from '../components/modules/PMS/Butler/butler';
import Language from '../components/modules/Main/Language.vue';
import GlobalSettings from '../components/modules/Main/GlobalSettings';
import DVR from '../components/modules/PMS/DVR/DVR';
import FindMissing from '../components/modules/PMS/FindMissing/FindMissing';
import FindMedia from '../components/modules/PMS/FindMedia/FindMedia';
import FindMediaSettings from '../components/modules/PMS/FindMedia/Settings/FindMediaSettings';
import LibraryMapping from '../components/modules/PMS/LibraryMapping/LibraryMapping';
import ViewState from '../components/modules/PMS/ViewState/ViewState';
import About from '../components/modules/Main/About';
@ -97,9 +98,15 @@ Vue.use(VueRouter)
meta: {requiresAuth: true}
},
{
path: '/pms/findmissing',
name: "FindMissing",
component: FindMissing,
path: '/pms/findmedia',
name: "FindMedia",
component: FindMedia,
meta: {requiresAuth: true}
},
{
path: '/pms/findmedia/Settings',
name: "FindMediaSettings",
component: FindMediaSettings,
meta: {requiresAuth: true}
},
{