Merge branch 'master' into #440-DVR-Module

This commit is contained in:
Tommy Mikkelsen 2022-03-25 00:46:34 +01:00
commit f412fc001b
20 changed files with 1958 additions and 850 deletions

View file

@ -1,6 +1,19 @@
# ![Logo](https://github.com/WebTools-NG/WebTools-NG/blob/master/src/assets/WebTools-48x48.png) WebTools-ng Change log
## V0.3.14 (Not yet released)
## V0.3.15 (Not released yet)
**Note**: This version is an Beta version
**Note 2**: In this version, the following is disabled:
* Export to xlsx format ([See #331](https://github.com/WebTools-NG/WebTools-NG/issues/331))
* Photo export
**Changes**:
* [#453 Kickstart Butler Scheduled tasks](https://github.com/WebTools-NG/WebTools-NG/issues/453)
## V0.3.14 (20220323)
**Note**: This version is an Beta version
@ -14,6 +27,7 @@
* [#429 Allow setting the LogNumFiles from Settings](https://github.com/WebTools-NG/WebTools-NG/issues/429)
* [#435 Showing Rel Notes should only show current version](https://github.com/WebTools-NG/WebTools-NG/issues/435)
* [#424 Export for severs with special charters in server name](https://github.com/WebTools-NG/WebTools-NG/issues/424)
* [#444 ET: Suggest better naming for movies only](https://github.com/WebTools-NG/WebTools-NG/issues/444)
## V0.3.13 (20220102)

2020
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{
"name": "webtools-ng",
"productName": "WebTools-NG",
"version": "0.3.14",
"version": "0.3.15",
"description": "WebTools Next Generation 4 Plex",
"author": "dane22 & CPSO",
"license": "MPL-2.0",
@ -45,7 +45,7 @@
"levenary": "^1.1.1",
"minimist": "^1.2.5",
"node-fetch": "^2.6.1",
"node-sass": "^6.0.0",
"node-sass": "^7.0.0",
"popper.js": "^1.16.1",
"rimraf": "^2.7.1",
"sanitize-filename": "^1.6.3",
@ -66,7 +66,7 @@
"@vue/cli-plugin-vuex": "^4.5.10",
"@vue/cli-service": "~4.3.0",
"babel-eslint": "^10.1.0",
"electron": "^11.2.3",
"electron": "^13.6.6",
"electron-devtools-installer": "^3.1.1",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",

View file

@ -111,7 +111,8 @@
"NavTitle": "Global Settings"
},
"PMS": {
"Settings": "Settings"
"Settings": "Settings",
"Butler": "Butler scheduled tasks"
}
}
},
@ -143,6 +144,8 @@
"TimeOut": "Timeout when requesting info from PMS in sec (Global setting)",
"OrgTitleNull": "Default \"Original Title\" to \"Title\", if empty",
"SortTitleNull": "Default \"Sort Title\" to \"Title\", if empty",
"suggestedFileNoExtra": "For suggested filename strip extra info from filenames so only essential info is kept",
"suggestedUseOrigenTitle": "When suggesting file or foldername, use original title",
"AutoXLSCol": "Autosize column (xlsx only)",
"AutoXLSRow": "Autosize row (xlsx only)",
"ExportToExcel": "Export to xlsx",
@ -156,8 +159,14 @@
"ChReturn": "Replace Return character with",
"ChReturn_TT": "Leave blank to not replace",
"ChNewLine": "Replace NewLine character with",
"ChNewLine_TT": "Leave blank to not replace"
"ChNewLine_TT": "Leave blank to not replace",
"MoviesUseId": "Default Id for suggested folder and filename for movies",
"ttMoviesUseId": "If exporting Suggested Filename or Folder for movies, default to selected naming if available. Fallback to IMDB if not",
"ShowsUseId": "Default Id for suggested folder and filename for shows",
"ttShowsUseId": "If exporting Suggested Filename or Folder for shows, default to selected naming if available. Fallback to TMDB if not"
},
"FolderNameOK": "** Folder named correctly **",
"FileNameOK": "** File named correctly **",
"LevelInfo": "Export level determines which data are going to be exported.",
"SelectLevel": "Select level",
"BuildInLevels": "*** Built-in levels ***",
@ -318,6 +327,29 @@
"Description": "@:Modules.PMS.Name module allows you to manage your server",
"ErrorNoServerSelectedTitle": "No server selected",
"ErrorNoServerSelectedMsg": "You need to select a server on the top of the screen",
"Butler": {
"Title": "Butler Scheduled Tasks",
"Description": "Here you can kickstart a scheduled task",
"SelectTask": "Select the scheduled task to run now",
"TTSelectTask": "Here you select among the scheduled tasks that kan be started now",
"BackupDatabase": "Backup your database",
"BuildGracenoteCollections": "Build Gracenote Collections",
"CheckForUpdates": "Check for updates",
"CleanOldBundles": "Clean old bundles",
"CleanOldCacheFiles": "Clean old Cache Files",
"DeepMediaAnalysis": "Deep Media Analysis",
"GenerateAutoTags": "Generate Auto Tags",
"GenerateChapterThumbs": "Generate Chapter Thumbs",
"GenerateMediaIndexFiles": "Generate Media Index Files",
"OptimizeDatabase": "Optimize Database",
"RefreshLibraries": "Refresh Libraries",
"RefreshLocalMedia": "Refresh Local Media",
"RefreshPeriodicMetadata": "Refresh Periodic Metadata",
"UpgradeMediaAnalysis": "Upgrade Media Analysis",
"RunTask": "Run selected task",
"TaskStarted": "The selected task has been started",
"TaskDetails": "To follow progress, see Plex WebClient Dashboard"
},
"Settings": {
"Settings": "Settings",
"Description": "Here you can define the settings for the selected server",

View file

@ -69,6 +69,11 @@
href: '/pms/settings',
title: this.$t("Common.Menu.Sidebar.PMS.Settings"),
icon: 'fa fa-cog'
},
{
href: '/pms/butler',
title: this.$t("Common.Menu.Sidebar.PMS.Butler"),
icon: 'fa fa-tasks'
}
]
},

View file

@ -46,6 +46,19 @@
v-model="cbSelected"
@change.native="filterTable">
</b-form-checkbox-group>
</b-form-group>
<b-form-group id="etSugMovieID" v-bind:label="$t('Modules.ET.Settings.MoviesUseId')" label-size="lg" label-class="font-weight-bold pt-0">
<b-tooltip target="etSugMovieID" triggers="hover">
{{ $t('Modules.ET.Settings.ttMoviesUseId') }}
</b-tooltip>
<b-form-select
class="form-control"
v-model="SelectedMoviesID"
id="SelectedMoviesID"
:options="SelectedMoviesIDOptions"
@change="SelectedMoviesIDChanged"
name="SugMovieID">
</b-form-select>
</b-form-group>
</div>
</b-container>
@ -54,12 +67,11 @@
<script>
const log = require("electron-log");
import {wtutils, wtconfig, dialog} from '../../General/wtutils'
log, wtutils, wtconfig, dialog
import i18n from '../../../../i18n'
export default {
created() {
this.getcbDefaults();
this.getDefaults();
if (wtconfig.get('ET.ColumnSep') == '\t')
{
this.ColumnSep = '{TAB}';
@ -83,24 +95,34 @@
{ text: i18n.t('Modules.ET.Settings.ExportToCSV'), value: 'ExpCSV' },
{ text: i18n.t('Modules.ET.Settings.ExportToExcel'), value: 'ExpXLSX', disabled: true },
{ text: i18n.t('Modules.ET.Settings.OrgTitleNull'), value: 'OrgTitleNull' },
{ text: i18n.t('Modules.ET.Settings.SortTitleNull'), value: 'SortTitleNull' }
{ text: i18n.t('Modules.ET.Settings.SortTitleNull'), value: 'SortTitleNull' },
{ text: i18n.t('Modules.ET.Settings.suggestedFileNoExtra'), value: 'suggestedFileNoExtra' },
{ text: i18n.t('Modules.ET.Settings.suggestedUseOrigenTitle'), value: 'suggestedUseOrigenTitle' }
],
ChReturn: wtconfig.get('ET.ChReturn', '<RETURN>'),
ChNewLine: wtconfig.get('ET.ChNewLine', '<NEWLINE>')
ChNewLine: wtconfig.get('ET.ChNewLine', '<NEWLINE>'),
SelectedMoviesIDOptions: ['imdb', 'tmdb'],
SelectedMoviesID: '',
SelectedShowsIDOptions: ['tmdb', 'tvdb'],
SelectedShowsID: ''
};
},
methods: {
getcbDefaults(){
const cbItems = ["ExpCSV","ExpXLSX", "OrgTitleNull", "SortTitleNull"];
getDefaults(){
const cbItems = ["ExpCSV","ExpXLSX", "OrgTitleNull", "SortTitleNull", "suggestedFileNoExtra", "suggestedUseOrigenTitle"];
for(let i = 0; i < cbItems.length; i++){
if (wtconfig.get("ET." + cbItems[i], false)){
this.cbSelected.push(cbItems[i]);
}
}
this.SelectedMoviesID = wtconfig.get("ET.SelectedMoviesID", "imdb");
},
SelectedMoviesIDChanged(){
wtconfig.set("ET.SelectedMoviesID", this.SelectedMoviesID);
},
filterTable(){
this.$nextTick(()=>{console.log(this.cbSelected);})
for( var cbItem of ["ExpCSV","ExpXLSX","OrgTitleNull", "SortTitleNull", "AutoXLSCol", "AutoXLSRow"]){
for( var cbItem of ["ExpCSV","ExpXLSX","OrgTitleNull", "SortTitleNull", "AutoXLSCol", "AutoXLSRow", "suggestedFileNoExtra", "suggestedUseOrigenTitle"]){
wtconfig.set("ET." + cbItem, (this.cbSelected.includes(cbItem)))
}
},

View file

@ -505,6 +505,12 @@
"type": "array-count",
"include": "includeExtras=1"
},
"File Path":
{
"key": "$.Media[0].Part[0].file",
"call": 1,
"type": "string"
},
"Filters":
{
"key": "$.filters",
@ -1195,6 +1201,20 @@
"subtype": "string",
"subkey": "$.title"
},
"Suggested File Name":
{
"key": "$.title",
"call": 1,
"type": "string",
"postProcess": true
},
"Suggested Folder Name":
{
"key": "$.title",
"call": 1,
"type": "string",
"postProcess": true
},
"Summary":
{
"key": "$.summary",

View file

@ -47,20 +47,21 @@
"levels": {
"Level 1": "level1",
"Level 2": "level2",
"All": "all"
"All": "all",
"Suggest Naming": "suggestnaming"
},
"LevelCount": {
"Level 1": 1,
"Level 2": 1,
"Suggest Naming": 1,
"All": 2,
"all": 2
},
"Include": {
"Level 11": "",
"Level 21": "",
"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",
"Missing1": "includeAllConcerts=1&includeChildren=1&includeConcerts=1&includeFields=1&includeGeolocation=1&includeLoudnessRamps=1&includeMarkers=1&includeOnDeck=1&includePopularLeaves=1&includePreferences=1&includeRelated=1&includeRelatedCount=1&includeReviews=1&includeStations=1",
"Everything1": "checkFiles=1&includeAllConcerts=1&includeBandwidths=1&includeChapters=1&includeChildren=1&includeConcerts=1&includeExtras=1&includeFields=1&includeGeolocation=1&includeLoudnessRamps=1&includeMarkers=1&includeOnDeck=1&includePopularLeaves=1&includePreferences=1&includeRelated=1&includeRelatedCount=1&includeReviews=1&includeStations=1"
"GED SLET Missing1": "includeAllConcerts=1&includeChildren=1&includeConcerts=1&includeFields=1&includeGeolocation=1&includeLoudnessRamps=1&includeMarkers=1&includeOnDeck=1&includePopularLeaves=1&includePreferences=1&includeRelated=1&includeRelatedCount=1&includeReviews=1&includeStations=1",
"GED SLET Everything1": "checkFiles=1&includeAllConcerts=1&includeBandwidths=1&includeChapters=1&includeChildren=1&includeConcerts=1&includeExtras=1&includeFields=1&includeGeolocation=1&includeLoudnessRamps=1&includeMarkers=1&includeOnDeck=1&includePopularLeaves=1&includePreferences=1&includeRelated=1&includeRelatedCount=1&includeReviews=1&includeStations=1"
}
},

View file

@ -35,6 +35,13 @@
"Audience Rating",
"User Rating"
],
"suggestnaming": [
"Title",
"Year",
"File Path",
"Suggested Folder Name",
"Suggested File Name"
],
"all": [
"Added",
"Art url",

View file

@ -60,10 +60,259 @@ function setQualifier( {str:str})
{
result = `${wtconfig.get('ET.TextQualifierCSV', 'N/A')}${str}${wtconfig.get('ET.TextQualifierCSV', 'N/A')}`
}
log.debug(`etHelper (setQualifier): Got: _WTNG_${str}_WTNG_ and returning ${result}`);
log.debug(`etHelper (setQualifier) - Got: _WTNG_${str}_WTNG_ and returning ${result}`);
return result;
}
// Clean up tmpFileName for suggested files/folders
// Remove leading and trailing spaces, as well as special characters
function cleanupSuggestedFile( tmpFileName )
{
const unWantedChars = '.-*_[](){}';
log.verbose(`etHelper (cleanupSuggestedFile) - starting Param: ${tmpFileName}`);
// Now replace square brackets if present with a dot
tmpFileName = tmpFileName.replaceAll("[", ".");
tmpFileName = tmpFileName.replaceAll("]", ".");
// Start by trimming the string
tmpFileName = tmpFileName.trim();
while ( unWantedChars.indexOf(tmpFileName.charAt(0)) > -1)
{
tmpFileName = tmpFileName.slice(1).trim();
if ( tmpFileName.length === 0 ) break;
}
// Remove from end of the string
while ( unWantedChars.indexOf(tmpFileName.charAt(tmpFileName.length-1)) > -1)
{
tmpFileName = tmpFileName.slice(0,-1).trim();
if ( tmpFileName.length === 0 ) break;
}
// Now replace double dots if present with a single dot
tmpFileName = tmpFileName.replaceAll("..", ".");
// Now delete empty brackets
tmpFileName = tmpFileName.replaceAll("()", "");
tmpFileName = tmpFileName.replaceAll("{}", "");
log.verbose(`etHelper (cleanupSuggestedFile) - Returning: ${tmpFileName}`);
return tmpFileName;
}
// Returns a suggested title for a media
function getSuggestedTitle( data )
{
let title;
if (wtconfig.get('ET.suggestedUseOrigenTitle', false))
{
title = String(JSONPath({path: '$.data.originalTitle', json: data})).replace(/[/\\?%*:|"<>]/g, '').replace(' ', ' ');
}
else
{
title = String(JSONPath({path: '$.data.title', json: data})).replace(/[/\\?%*:|"<>]/g, '').replace(' ', ' ');
}
// Original selected, but none exist, we default to named title
if ( title === '')
{
title = String(JSONPath({path: '$.data.title', json: data})).replace(/[/\\?%*:|"<>]/g, '').replace(' ', ' ');
}
return title;
}
// Returns a suggested year for a media
function getSuggestedYear( data )
{
return String(JSONPath({path: '$.data.year', json: data}));
}
// Returns a suggested id for a media
function getSuggestedId( data )
{
log.verbose(`etHelper (getSuggestedId) - Started. To see Param, switch to Silly logging`);
log.silly(`etHelper (getSuggestedId) - Starting with param: ${JSON.stringify(data)}`);
let imdb = String(JSONPath({path: "$.data.Guid[?(@.id.startsWith('imdb'))].id", json: data}));
let tmdb = String(JSONPath({path: "$.data.Guid[?(@.id.startsWith('tmdb'))].id", json: data}));
let tvdb = String(JSONPath({path: "$.data.Guid[?(@.id.startsWith('tvdb'))].id", json: data}));
if (imdb === ''){
imdb = "imdb://" + String(JSONPath({path: "$.data.guid", json: data})).substring(26,).split('?')[0];
}
// Fallback to imdb, if tmdb is not present
if (tmdb === '')
{
tmdb = imdb;
}
// Fallback to imdb, if tvdb is not present
if (tvdb === '')
{
tvdb = imdb;
}
let selId;
if ( etHelper.Settings.libType === 1)
{
selId = wtconfig.get("ET.SelectedMoviesID", "imdb");
}
else
{
selId = wtconfig.get("ET.SelectedShowsID", "tmdb");
}
log.silly(`etHelper (getSuggestedId) - imdb ID: ${imdb}`);
log.silly(`etHelper (getSuggestedId) - tmdb ID: ${tmdb}`);
log.silly(`etHelper (getSuggestedId) - tvdb ID: ${tvdb}`);
let Id;
switch(selId) {
case 'imdb':
Id = `{imdb-${imdb.slice(7)}}`;
break;
case 'tmdb':
if (tmdb === '')
{
Id = `{imdb-${imdb.slice(7)}}`;
}
else
{
Id = `{tmdb-${tmdb.slice(7)}}`;
}
break;
case 'tvdb':
if (tvdb === '')
{
Id = `{imdb-${imdb.slice(7)}}`;
}
else
{
Id = `{tvdb-${tvdb.slice(7)}}`;
}
break;
}
log.debug(`etHelper (getSuggestedId) - Returning: "imdb": ${imdb}, "tmdb": ${tmdb}, "tvdb": ${tvdb}, "selId": ${Id}`);
return {"imdb": imdb, "tmdb": tmdb, "tvdb": tvdb, "selId": Id};
}
// Returns a string stripped for Year
function stripYearFromFileName( tmpFileName, year ){
const re = new RegExp(`\\b${year}\\b`, 'gi');
return tmpFileName.replace(re, "").trim();
}
// Returns a string stripped for ID's
function stripIdFromFileName( param ){
log.debug(`etHelper (stripIdFromFileName) - starting function with param as: ${JSON.stringify(param)}`);
let tmpFileName = param.tmpFileName;
let re;
const imdb = param.imdb.slice(7);
const tmdb = param.tmdb.slice(7);
const tvdb = param.tvdb.slice(7);
// Remove IMDB id
log.debug(`etHelper (stripIdFromFileName) - Imdb string is : ${imdb}`);
re = new RegExp(`\\b${imdb}\\b`, 'gi');
tmpFileName = tmpFileName.replace(re, "");
log.debug(`etHelper (stripIdFromFileName) - After imdb id is removed: ${tmpFileName}`);
tmpFileName = tmpFileName.replace(/imdb-/i, '');
log.debug(`etHelper (stripIdFromFileName) - After imdb string is removed: ${tmpFileName}`);
// Remove TMDB id
re = new RegExp(`\\b${tmdb}\\b`, 'gi');
tmpFileName = tmpFileName.replace(re, "");
// Remove TVDB id
re = new RegExp(`\\b${tvdb}\\b`, 'gi');
tmpFileName = tmpFileName.replace(re, "");
const idProviders = ['imdb', 'tmdb', 'tvdb'];
idProviders.forEach(element => {
re = new RegExp(`\\b${element}\\b`, 'gi');
tmpFileName = tmpFileName.replace(re, "");
});
return tmpFileName.replace(`{-}`, "").trim();
}
// Returns a filename without the title
function stripTitleFromFileName( tmpFileName, title )
{
let re;
// Title in filename separated with dots
let titleArray = tmpFileName.split(".");
for (let titleElement of titleArray) {
if (title.toUpperCase().indexOf(titleElement.toUpperCase()) > -1) {
re = new RegExp(`\\b${titleElement}\\b`, 'gi');
tmpFileName = tmpFileName.replace(re, "");
}
if (titleElement.toUpperCase() === 'AND')
{
titleElement = '&'
if (title.toUpperCase().indexOf(titleElement.toUpperCase()) > -1) {
re = new RegExp(`\\band\\b`, 'gi');
tmpFileName = tmpFileName.replace(re, "");
}
}
}
// Title in filename separated with spaces
titleArray = tmpFileName.split(" ");
for (let titleElement of titleArray) {
if (title.toUpperCase().indexOf(titleElement.toUpperCase()) > -1) {
re = new RegExp(`\\b${titleElement}\\b`, 'gi');
tmpFileName = tmpFileName.replace(re, "");
}
if (titleElement == '&')
{
titleElement = 'AND'
if (title.toUpperCase().indexOf(titleElement.toUpperCase()) > -1) {
re = new RegExp(`\\b${titleElement}\\b`, 'gi');
tmpFileName = tmpFileName.replace(re, "");
}
}
}
tmpFileName = tmpFileName.trim();
return tmpFileName;
}
// Strip parts from a filename, and return multiple values
function stripPartsFromFileName( tmpFileName, title ) {
log.verbose(`etHelper (stripPartsFromFileName) - looking at ${tmpFileName}`);
let partName = '';
// Find stacked item if present
etHelper.StackedFilesName.forEach(element => {
// Got a hit?
if (tmpFileName.toLowerCase().indexOf(element) > -1) {
// Get index again
const idx = tmpFileName.toLowerCase().indexOf(element);
// Got a stacked identifier, so make sure next character is a number in [1-8] range
const numStacked = tmpFileName.charAt(idx + element.length);
if (isNaN(numStacked)) {
log.info(`etHelper (stripPartsFromFileName) - for the media with the title: "${title}" looking at the string: "${tmpFileName}" for stacked element: "${element}" but found next character not a number so ignorring`)
}
else
{
if (parseInt(numStacked, 10) < 9 && parseInt(numStacked, 10) > 0)
{
// Extract element part, but add one char more for the counter
partName = tmpFileName.substring(idx, idx + element.length + 1).toLowerCase();
}
else
{
log.warn(`etHelper (stripPartsFromFileName) - for the media with the title: "${title}" looking at the string: "${tmpFileName}" for stacked element: "${element}" but found entry not in range [1-8] so ignorring`)
log.warn(`etHelper (stripPartsFromFileName) - See: https://support.plex.tv/articles/naming-and-organizing-your-movie-media-files/`)
}
}
}
});
// Remove partName from tmpFile
// Get index of partName
const idx = tmpFileName.toUpperCase().indexOf(partName.toUpperCase());
tmpFileName = tmpFileName.slice(0, idx) + tmpFileName.slice(idx + partName.length);
// Remove white spaces
tmpFileName = tmpFileName.trim();
if (tmpFileName.length === 1)
{
if (tmpFileName === '.'){
tmpFileName = '';
}
if (tmpFileName === '-'){
tmpFileName = '';
}
}
log.verbose(`etHelper (stripPartsFromFileName) - Returning tmpFileName as: ${tmpFileName} *** Returning partName as: ${partName}`);
return {
fileName: tmpFileName,
partName: partName
};
}
//#endregion
const etHelper = new class ETHELPER {
@ -110,7 +359,9 @@ const etHelper = new class ETHELPER {
selType: null,
fileMajor: null,
fileMinor: null,
element: null
element: null,
SelectedMoviesID: null,
SelectedShowsID: wtconfig.get("ET.SelectedShowsID", "tmdb")
};
this.PMSHeader = wtutils.PMSHeader;
@ -175,6 +426,7 @@ const etHelper = new class ETHELPER {
'TimeElapsed': 8,
'RunningTime': 9
};
this.StackedFilesName = ['cd', 'disc', 'dvd', 'part', 'pt'];
}
resetETHelper() {
@ -208,10 +460,125 @@ const etHelper = new class ETHELPER {
}
}
async postProcess( {name, val, title=""} ){
log.debug(`ETHelper(postProcess): Val is: ${JSON.stringify(val)}`);
log.debug(`ETHelper(postProcess): name is: ${name}`);
log.debug(`ETHelper(postProcess): title is: ${title}`);
/// This will return a suggested foldername, following Plex naming std
async getSuggestedFolderName( data )
{
log.verbose(`etHelper (getSuggestedFolderName) - Starting function. To see param, use Silly log level`);
log.silly(`etHelper (getSuggestedFolderName) - Data pased over as: ${JSON.stringify(data)}`);
const title = getSuggestedTitle( data );
const year = getSuggestedYear( data );
const Id = getSuggestedId( data ).selId;
// Get current folder name
const curFolderName = path.basename(path.dirname(String(JSONPath({path: "$.data.Media[0].Part[0].file", json: data}))))
// Compute suggested foldername
let foldername = `${title} (${year}) ${Id}`;
log.debug(`etHelper (getSuggestedFolderName) - Suggested folderName is: ${foldername}`);
if (curFolderName === foldername) {
return i18n.t("Modules.ET.FolderNameOK")
}
else {
return foldername
}
}
/// This will return a suggested filename, following Plex naming std
async getSuggestedFileName( data )
{
const title = getSuggestedTitle( data );
const year = getSuggestedYear( data );
const Ids = getSuggestedId( data );
const Id = Ids.selId;
const imdb = Ids.imdb;
const tmdb = Ids.tmdb;
const tvdb = Ids.tvdb;
// Get current filename
const curFileName = path.parse(String(JSONPath({path: '$.data.Media[0].Part[0].file', json: data}))).name;
// Compute suggested filename, and start by stripping known info from existing name
// Start with the title
let tmpFileName = stripTitleFromFileName( curFileName, title );
// Strip Year from fileName
tmpFileName = stripYearFromFileName( tmpFileName, year );
// Strip ID from fileName
tmpFileName = stripIdFromFileName( {tmpFileName: tmpFileName, imdb: imdb, tmdb: tmdb, tvdb: tvdb} );
tmpFileName = cleanupSuggestedFile(tmpFileName);
// Get parts, if a stacked media
const parts = stripPartsFromFileName( tmpFileName, title );
tmpFileName = parts.fileName;
const partName = parts.partName;
tmpFileName = cleanupSuggestedFile(tmpFileName);
//cleanupSuggestedFile(tmpFileName);
/*
// Remove empty brackets if present
tmpFileName = tmpFileName.replaceAll("()", "");
// Remove empty curly brackets if present
tmpFileName = tmpFileName.replaceAll("{}", "");
// Now replace square brackets if present with a dot
tmpFileName = tmpFileName.replaceAll("[", ".");
tmpFileName = tmpFileName.replaceAll("]", ".");
// Remove double dots if present
tmpFileName = tmpFileName.replaceAll("..", ".");
tmpFileName = tmpFileName.trim();
// Remove leading dots if present
while(tmpFileName.charAt(0) === '.')
{
tmpFileName = tmpFileName.substring(1);
}
// Remove trailing dots if present
while(tmpFileName.slice(-1) === '.')
{
tmpFileName = tmpFileName.substring(0, tmpFileName.length - 1);
}
// Remove double square brackets if present
tmpFileName = tmpFileName.replaceAll("[]", "");
// Remove double square brackets with a space if present
tmpFileName = tmpFileName.replaceAll("[ ]", "");
// Remove double space if present
tmpFileName = tmpFileName.replaceAll(" ", " ");
// Remove space dot if present, and replace with dot
tmpFileName = tmpFileName.replaceAll(" .", ".");
tmpFileName = tmpFileName.trim();
*/
// Get filename part of remaining filename
if (tmpFileName.length >= 1)
{
tmpFileName = `[${tmpFileName}]`
}
let suggestedFileName;
if (wtconfig.get("ET.suggestedFileNoExtra", false))
{
suggestedFileName = `${title} (${year}) ${Id} ${partName}`.trim();
}
else
{
suggestedFileName = `${title} (${year}) ${Id} ${tmpFileName} ${partName}`.trim();
}
// Remove double space if present
suggestedFileName = suggestedFileName.replaceAll(" ", " ");
const fileNameExt = path.parse(String(JSONPath({path: '$.data.Media[0].Part[0].file', json: data}))).ext;
suggestedFileName = `${suggestedFileName}${fileNameExt}`;
log.debug(`etHelper (getSuggestedFileName) - returning ${suggestedFileName}`);
if (curFileName === path.parse(suggestedFileName).name) {
return i18n.t("Modules.ET.FileNameOK")
}
else {
return suggestedFileName
}
}
async postProcess( {name, val, title="", data} ){
log.debug(`ETHelper(postProcess) - Val is: ${JSON.stringify(val)}`);
log.debug(`ETHelper(postProcess) - name is: ${name}`);
log.debug(`ETHelper(postProcess) - title is: ${title}`);
let retArray = [];
let guidArr;
let x, retVal, start, strStart, end, result;
@ -228,6 +595,12 @@ const etHelper = new class ETHELPER {
retVal = val.substring(0, 3);
break;
}
case "Suggested File Name":
retVal = await this.getSuggestedFileName( {data: data} );
break;
case "Suggested Folder Name":
retVal = await this.getSuggestedFolderName( {data: data} );
break;
case "Part File":
for (x=0; x<valArray.length; x++) {
retArray.push(path.basename(valArray[x]).slice(0, -1));
@ -457,7 +830,7 @@ const etHelper = new class ETHELPER {
}
} catch (error) {
retVal = 'ERROR'
log.error(`ETHelper(postProcess): We had an error as: ${error} . So postProcess retVal set to ERROR`);
log.error(`ETHelper(postProcess) - We had an error as: ${error} . So postProcess retVal set to ERROR`);
}
return await retVal;
}
@ -592,8 +965,8 @@ const etHelper = new class ETHELPER {
if ( doPostProc )
{
const title = JSONPath({path: String('$.title'), json: data})[0];
log.debug(`ETHelper(addRowToTmp doPostProc): Name is: ${name} - Title is: ${title} - Val is: ${val}`)
val = await this.postProcess( {name: name, val: val, title: title} );
log.debug(`ETHelper(addRowToTmp doPostProc) - Name is: ${name} - Title is: ${title} - Val is: ${val}`)
val = await this.postProcess( {name: name, val: val, title: title, data: data} );
}
// Here we add qualifier, if not a number
if (!['array-count', 'int'].includes(type))
@ -676,7 +1049,7 @@ const etHelper = new class ETHELPER {
}
async populateExpFiles(){
log.info('etHelper(populateExpFiles): Populating export files');
log.info('etHelper(populateExpFiles) - Populating export files');
// Current item counter in the chunck
//let idx = 0;
let idx = this.Settings.startItem;
@ -697,7 +1070,7 @@ const etHelper = new class ETHELPER {
//chunck = await this.getItemData({postURI: postURI + idx});
chunck = await this.getSectionData();
size = JSONPath({path: '$.MediaContainer.size', json: chunck});
log.debug(`etHelper(populateExpFiles): Fetched a chunck with number of items as ${size} and contained: ${JSON.stringify(chunck)}`);
log.debug(`etHelper(populateExpFiles) - Fetched a chunck with number of items as ${size} and contained: ${JSON.stringify(chunck)}`);
if ( this.Settings.libType == this.ETmediaType.Libraries)
{
chunckItems = JSONPath({path: '$.MediaContainer.Directory.*', json: chunck});
@ -750,13 +1123,13 @@ const etHelper = new class ETHELPER {
}
idx = Number(idx) + Number(step);
} while (this.Settings.count < this.Settings.endItem);
log.info('etHelper(populateExpFiles): Populating export files ended');
log.info('etHelper(populateExpFiles) - Populating export files ended');
}
async getSectionSize()
{
log.debug(`etHelper (getSectionSize): selType: ${this.Settings.selType}`)
log.debug(`etHelper (getSectionSize): libTypeSec: ${this.Settings.libTypeSec}`)
log.debug(`etHelper (getSectionSize) - selType: ${this.Settings.selType}`)
log.debug(`etHelper (getSectionSize) - libTypeSec: ${this.Settings.libTypeSec}`)
let url = '';
switch(this.Settings.selType) {
case this.ETmediaType.Playlist_Video:
@ -802,14 +1175,14 @@ const etHelper = new class ETHELPER {
async getSectionData()
{
log.debug(`etHelper (getSectionData): Element is: ${this.Settings.element}`)
log.debug(`etHelper (getSectionData) - Element is: ${this.Settings.element}`)
//const url = this.Settings.baseURL + this.Settings.element + '?' + this.getPostURI() + this.Settings.count;
const url = this.Settings.baseURL + this.Settings.element + this.getPostURI() + this.Settings.count;
this.PMSHeader["X-Plex-Token"] = this.Settings.accessToken;
log.verbose(`etHelper (getSectionData): Calling url in getSectionData: ${url}`)
log.verbose(`etHelper (getSectionData) - Calling url in getSectionData: ${url}`)
let response = await fetch(url, { method: 'GET', headers: this.PMSHeader});
let resp = await response.json();
log.debug(`etHelper (getSectionData): Response in getSectionData: ${JSON.stringify(resp)}`)
log.debug(`etHelper (getSectionData) - Response in getSectionData: ${JSON.stringify(resp)}`)
return resp
}
@ -818,7 +1191,7 @@ const etHelper = new class ETHELPER {
{
this.Settings.libType = this.Settings.libTypeSec;
}
log.debug(`etHelper (getLevelCall): LibType: ${this.Settings.libTypeSec} * LevelName: ${this.Settings.levelName}`);
log.debug(`etHelper (getLevelCall) - LibType: ${this.Settings.libTypeSec} * LevelName: ${this.Settings.levelName}`);
let count = await defLevels[this.Settings.libTypeSec]['LevelCount'][this.Settings.levelName]
if (count == undefined)
{
@ -1112,20 +1485,6 @@ const etHelper = new class ETHELPER {
// [{"title":"DVR Movies","key":31,"type":"movie"}]
const result = [];
let url = address + '/library/sections/all'
console.log('Ged 5-4 Type: ' + this.Settings.selType)
/*
if ([this.ETmediaType.Playlist_Audio, this.ETmediaType.Playlist_Video].includes(this.Settings.selType))
{
url = address + '/library/sections/all?type=15&sort=lastViewedAt:desc&playlistType=video,audio'
}
else
{
url = address + '/library/sections/all'
}
*/
this.PMSHeader["X-Plex-Token"] = accessToken;
let response = await fetch(url, { method: 'GET', headers: this.PMSHeader});
let resp = await response.json();
@ -1170,7 +1529,6 @@ const etHelper = new class ETHELPER {
getElement(){
let element
console.log('Ged 11 SecType: ' + this.Settings.libTypeSec)
switch (this.Settings.libTypeSec) {
case this.ETmediaType.Playlist_Photo:
element = `/playlists/${this.Settings.selLibKey}/items`;
@ -1199,6 +1557,7 @@ const etHelper = new class ETHELPER {
getIncludeInfo(){
let includeInfo;
log.debug(`etHelper (getIncludeInfo) - Started. libTypeSec is: ${this.Settings.libTypeSec} and levelName is: ${this.Settings.levelName}`);
try {
includeInfo = defLevels[this.Settings.libTypeSec]['Include'][this.Settings.levelName];
}
@ -1213,7 +1572,7 @@ const etHelper = new class ETHELPER {
{
includeInfo = ''
}
log.debug(`etHelper (getInclude): returning: ${includeInfo}`);
log.debug(`etHelper (getInclude) - returning: ${includeInfo}`);
return includeInfo;
}
@ -1221,9 +1580,9 @@ const etHelper = new class ETHELPER {
let postURI, includeInfo;
// Find LibType steps
const step = wtconfig.get("PMS.ContainerSize." + this.Settings.libType, 20);
log.debug(`etHelper (getPostURI): Got Step size as: ${step}`);
log.debug(`etHelper (getPostURI): libType is: ${this.Settings.libType}`);
log.debug(`etHelper (getPostURI): libTypeSec is: ${this.Settings.libTypeSec}`);
log.debug(`etHelper (getPostURI) - Got Step size as: ${step}`);
log.debug(`etHelper (getPostURI) - libType is: ${this.Settings.libType}`);
log.debug(`etHelper (getPostURI) - libTypeSec is: ${this.Settings.libTypeSec}`);
switch (this.Settings.libType) {
case this.ETmediaType.Photo:
postURI = `?addedAt>>=-2208992400&X-Plex-Container-Size=${step}&type=${this.Settings.libTypeSec}&${this.uriParams}&X-Plex-Container-Start=`;
@ -1248,7 +1607,7 @@ const etHelper = new class ETHELPER {
break;
default:
includeInfo = this.getIncludeInfo();
log.debug(`etHelper (getPostURI): includeInfo is: ${includeInfo}`);
log.debug(`etHelper (getPostURI) - includeInfo is: ${includeInfo}`);
if (includeInfo != '')
{
postURI = `?X-Plex-Container-Size=${step}&type=${this.Settings.libTypeSec}&${includeInfo}&X-Plex-Container-Start=`;
@ -1258,7 +1617,7 @@ const etHelper = new class ETHELPER {
postURI = `?X-Plex-Container-Size=${step}&type=${this.Settings.libTypeSec}&X-Plex-Container-Start=`;
}
}
log.debug(`etHelper (getPostURI): Got postURI as ${postURI}`);
log.debug(`etHelper (getPostURI) - Returning postURI as ${postURI}`);
return postURI;
}
@ -1378,29 +1737,29 @@ const etHelper = new class ETHELPER {
// Public methode to get the Header
async getFieldHeader() {
log.info('etHelper (getFieldHeader): FieldHeader requested');
log.info('etHelper (getFieldHeader) - FieldHeader requested');
try{
if (isEmptyObj(this.#_FieldHeader))
{
log.verbose(`etHelper(getFieldHeader): Need to generate the header`);
log.verbose(`etHelper(getFieldHeader) - Need to generate the header`);
this.#_FieldHeader = await etHelper.#SetFieldHeader()
}
else
{
log.verbose(`etHelper(getFieldHeader): Returning cached headers`);
log.verbose(`etHelper(getFieldHeader) - Returning cached headers`);
}
}
catch (error)
{
log.error(`etHelper(getFieldHeader): ${error}`);
log.error(`etHelper(getFieldHeader) - ${error}`);
}
log.verbose(`etHelper(getFieldHeader): Field header is: ${JSON.stringify(this.#_FieldHeader)}`);
log.verbose(`etHelper(getFieldHeader) - Field header is: ${JSON.stringify(this.#_FieldHeader)}`);
return this.#_FieldHeader;
}
// Private methode to set the header
async #SetFieldHeader(){
log.verbose(`etHelper (SetFieldHeader): GetFieldHeader level: ${this.Settings.Level} - libType: ${this.Settings.libType}`);
log.verbose(`etHelper (SetFieldHeader) - GetFieldHeader level: ${this.Settings.Level} - libType: ${this.Settings.libType}`);
return await this.getLevelFields();
}
//#endregion

View file

@ -136,7 +136,8 @@ export default {
this.ver = releases['relver'];
this.beta = false;
}
if (wtutils.AppVersion != this.ver && this.ver)
const compVer = wtutils.AppVersion.substr(0, wtutils.AppVersion.lastIndexOf("."));
if (compVer != this.ver && this.ver)
{
// Show an update is present
if (this.ver == wtconfig.get('Update.SkipVer', ''))

View file

@ -0,0 +1,152 @@
<template>
<b-container fluid>
<div class="col-lg-10 col-md-12 col-xs-12">
<h1>{{ $t("Modules.PMS.Butler.Title") }}</h1>
<p>{{ $t("Modules.PMS.Butler.Description") }}</p>
</div>
<div class="d-flex align-items-center">
<b-form-group id="ButlerGroup" v-bind:label="$t('Modules.PMS.Butler.SelectTask')" label-size="lg" label-class="font-weight-bold pt-0">
<b-tooltip target="ButlerGroup" triggers="hover">
{{ $t('Modules.PMS.Butler.TTSelectTask') }}
</b-tooltip>
<b-form-select
v-model="selTask"
id="selTask"
:options="selTaskOptions"
name="selTask">
</b-form-select>
</b-form-group>
</div>
<br>
<br>
<div class="buttons">
<!-- Buttons -->
<div id="buttons" class="text-center">
<b-button-group >
<b-button variant="success" class="mr-1" :disabled="this.selTask == ''" @click="executeButlerTask"> {{ $t('Modules.PMS.Butler.RunTask') }} </b-button>
</b-button-group>
</div>
</div>
</b-container>
</template>
<script>
const log = require("electron-log");
import i18n from '../../../../i18n';
import store from '../../../../store';
export default {
data() {
return {
selTaskOptions: [
{
"text": i18n.t('Modules.PMS.Butler.BackupDatabase'),
"value": "BackupDatabase"
},
{
"text": i18n.t('Modules.PMS.Butler.BuildGracenoteCollections'),
"value": "BuildGracenoteCollections"
},
{
"text": i18n.t('Modules.PMS.Butler.CheckForUpdates'),
"value": "CheckForUpdates"
},
{
"text": i18n.t('Modules.PMS.Butler.CleanOldBundles'),
"value": "CleanOldBundles"
},
{
"text": i18n.t('Modules.PMS.Butler.CleanOldCacheFiles'),
"value": "CleanOldCacheFiles"
},
{
"text": i18n.t('Modules.PMS.Butler.DeepMediaAnalysis'),
"value": "DeepMediaAnalysis"
},
{
"text": i18n.t('Modules.PMS.Butler.GenerateAutoTags'),
"value": "GenerateAutoTags"
},
{
"text": i18n.t('Modules.PMS.Butler.GenerateChapterThumbs'),
"value": "GenerateChapterThumbs"
},
{
"text": i18n.t('Modules.PMS.Butler.GenerateMediaIndexFiles'),
"value": "GenerateMediaIndexFiles"
},
{
"text": i18n.t('Modules.PMS.Butler.OptimizeDatabase'),
"value": "OptimizeDatabase"
},
{
"text": i18n.t('Modules.PMS.Butler.RefreshLibraries'),
"value": "RefreshLibraries"
},
{
"text": i18n.t('Modules.PMS.Butler.RefreshLocalMedia'),
"value": "RefreshLocalMedia"
},
{
"text": i18n.t('Modules.PMS.Butler.RefreshPeriodicMetadata'),
"value": "RefreshPeriodicMetadata"
},
{
"text": i18n.t('Modules.PMS.Butler.UpgradeMediaAnalysis'),
"value": "UpgradeMediaAnalysis"
}
],
selTask : "",
};
},
created() {
log.info("PMS Butler Created");
this.serverSelected();
},
computed: {
selectedServerAddress: function(){
return this.$store.getters.getSelectedServerAddress;
}
},
methods: {
async executeButlerTask() {
log.debug(`Starting Butler Task: ${this.selTask}`);
await store.dispatch('startButlerTask', {
Token: this.$store.getters.getAuthToken,
Address: this.$store.getters.getSelectedServerAddress,
Job: this.selTask});
this.$bvToast.toast(this.$t("Modules.PMS.Butler.TaskDetails"), {
title: this.$t("Modules.PMS.Butler.TaskStarted"),
autoHideDelay: 4000,
solid: true,
variant: 'primary',
toaster: 'b-toaster-bottom-right'
});
},
async serverSelected() {
let serverCheck = this.$store.getters.getSelectedServer;
if (serverCheck == "none") {
log.debug("serverCheck is none");
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>
<style scoped>
.outDirbox{
margin-right:10px;
}
#b-form-group{
margin-top: 20px;
}
</style>

View file

@ -150,7 +150,6 @@
this.serverSelected();
this.getFilterSettings();
this.getServerSettings();
this.getcbDefaults();
},
computed: {
selectedServerAddress: function(){

View file

@ -8,6 +8,7 @@ import ExportCustom from '../components/modules/ExportTools/Custom/custom';
import PlexTV from '../components/modules/PlexTV/PlexTV';
import PMS from '../components/modules/PMS/PMS';
import PMSSettings from '../components/modules/PMS/Settings/settings';
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/DVR/DVR';
@ -69,6 +70,12 @@ Vue.use(VueRouter)
component: PMSSettings,
meta: {requiresAuth: true}
},
{
path: '/pms/butler',
name: "butler",
component: Butler,
meta: {requiresAuth: true}
},
{
path: '/plextv',
name: "plextv",

View file

@ -20,6 +20,35 @@ const getters = {
}
const actions = {
async startButlerTask({ commit }, payload) {
commit
let header = wtutils.PMSHeader;
header['X-Plex-Token'] = payload.Token;
const url = `${payload.Address}/butler/${payload.Job}`;
log.debug(`Setting new setting with url ${url}`);
await axios({
method: 'post',
url: url,
headers: header
})
.then((response) => {
log.debug('Response from startButlerTask recieved')
response
})
.catch(function (error) {
if (error.response) {
log.error('startButlerTask: ' + error.response.data)
alert(error.response.data.errors[0].code + " " + error.response.data.errors[0].message)
} else if (error.request) {
log.error('startButlerTask: ' + error.request)
} else {
log.error('startButlerTask: ' + error.message)
}
});
},
async setPMSSetting({ commit }, payload) {
commit

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB