diff --git a/CHANGELOG.md b/CHANGELOG.md index 4652c5c..b73c995 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ * [#469 Default ExportTools Levels do NOT produce any output](https://github.com/WebTools-NG/WebTools-NG/issues/469) * [#468 Add "Last Viewed at" as an available field for TV Show Episode exporting](https://github.com/WebTools-NG/WebTools-NG/issues/468) * [#473 PMS Root page](https://github.com/WebTools-NG/WebTools-NG/issues/473) +* [#459 Viewstate copy](https://github.com/WebTools-NG/WebTools-NG/issues/459) + ## V0.3.15 (20220413) diff --git a/public/locales/en.json b/public/locales/en.json index 6435c75..3d64b3f 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -130,7 +130,28 @@ "Browse": "Browse", "ErrorNoOutDirMsg": "You need to define an output directory in the global settings page first", "ErrorNoOutDirTitle": "No output directory defined", - "ReleaseNoteTitle": "Release Note" + "ReleaseNoteTitle": "Release Note", + "Status": { + "Name": { + "Global": { + "Status": "Status", + "Chuncks": "Chunks", + "Items": "Items", + "Info": "Info", + "OutFile": "Output File", + "StartTime": "Start time", + "EndTime": "End Time", + "TimeElapsed": "Time Elapsed", + "RunningTime": "Running time" + }, + "Modules": {} + }, + "Msg": { + "Global": { + }, + "Modules": {} + } + } }, "Modules": { "ET": { @@ -422,13 +443,17 @@ "Msg": { "CollectUserInfo": "Collecting user info...Please wait", "Processing": "Processing", + "Processing2": "Processing library {0} of {1} - {2}", "Idle": "Idle", - "GatheringLibs": "Gathering libraries to process" + "GatheringLibs": "Gathering libraries to process", + "ProcessItem1": "Processing: ({1} - {2}) - {0}" }, "Names": { "Status": "Status", "LibsToProcess": "Libraries to process", - "StartTime": "Start time" + "StartTime": "Start time", + "CurrentLib": "Current Library", + "Item": "Item" } } } diff --git a/src/components/modules/ExportTools/defs/def-Levels.json b/src/components/modules/ExportTools/defs/def-Levels.json index ccb28e2..9db8dba 100644 --- a/src/components/modules/ExportTools/defs/def-Levels.json +++ b/src/components/modules/ExportTools/defs/def-Levels.json @@ -59,9 +59,7 @@ }, "Include": { "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", - "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" + "all": "checkFiles=1&includeExtras=1&includeBandwidths=1&includeChapters=1" } }, diff --git a/src/components/modules/PMS/ViewState/ViewState.vue b/src/components/modules/PMS/ViewState/ViewState.vue index 1514867..0088718 100644 --- a/src/components/modules/PMS/ViewState/ViewState.vue +++ b/src/components/modules/PMS/ViewState/ViewState.vue @@ -114,12 +114,11 @@ // Watch for when selected server address is updated selectedServerAddress: async function(){ log.info("ViewState selected server changed"); - console.log('Ged 1-1: ' + JSON.stringify(this.$store.getters.getViewStateStatus)) viewstate.clearStatus(); console.log('Ged 1-2: ' + JSON.stringify(this.$store.getters.getViewStateStatus)) viewstate.updateStatusMsg(1, i18n.t("Modules.PMS.ViewState.Status.Msg.CollectUserInfo")); - viewstate.SrcUsrKey = -1; - viewstate.TargetUsrKey = -1; + viewstate.SrcUsr = null; + viewstate.TargetUsr = null; console.log('Ged 1-3: ' + JSON.stringify(this.$store.getters.getViewStateStatus)) this.serverIsSelected = ( this.$store.getters.getSelectedServer != "none" ); this.WaitForUsers = false; @@ -156,12 +155,16 @@ methods: { // SrcUsr changed async selSrcUsrChanged() { - await viewstate.setKey( 'selSrcUsr', this.selSrcUsr); + viewstate.SrcUsr = this.selSrcUsr; + await viewstate.setOwnerStatus( 'selSrcUsr', this.selSrcUsr); await viewstate.getLibs( this.selSrcUsr, this.selTargetUsr ); + + console.log('Ged 14-1 Src: ' + JSON.stringify(viewstate.SrcUsr)) }, // SrcUsr changed async selTargetUsrChanged() { - await viewstate.setKey( 'selTargetUsr', this.selTargetUsr ); + viewstate.TargetUsr = this.selTargetUsr; + await viewstate.setOwnerStatus( 'selTargetUsr', this.selTargetUsr ); await viewstate.getLibs( this.selSrcUsr, this.selTargetUsr ); }, /* Check if a server is selected, and if not diff --git a/src/components/modules/PMS/ViewState/scripts/viewstate.js b/src/components/modules/PMS/ViewState/scripts/viewstate.js index 72cda58..d85de84 100644 --- a/src/components/modules/PMS/ViewState/scripts/viewstate.js +++ b/src/components/modules/PMS/ViewState/scripts/viewstate.js @@ -4,7 +4,7 @@ const {JSONPath} = require('jsonpath-plus'); console.log = log.log; -import {wtutils} from '../../../General/wtutils'; +import {wtutils, wtconfig} from '../../../General/wtutils'; import i18n from '../../../../../i18n'; import store from '../../../../../store'; import axios from 'axios'; @@ -18,30 +18,46 @@ const viewstate = new class ViewState { #_msgType = { 1: i18n.t("Modules.PMS.ViewState.Status.Names.Status"), 2: i18n.t("Modules.PMS.ViewState.Status.Names.LibsToProcess"), - 3: i18n.t("Modules.PMS.ViewState.Status.Names.StartTime") - + 3: i18n.t("Modules.PMS.ViewState.Status.Names.StartTime"), + 4: i18n.t("Modules.PMS.ViewState.Status.Names.CurrentLib"), + 5: i18n.t("Modules.PMS.ViewState.Status.Names.Item"), + 6: i18n.t("Modules.ET.Status.Names.StartTime"), + 7: i18n.t("Modules.ET.Status.Names.EndTime"), + 8: i18n.t("Modules.ET.Status.Names.TimeElapsed"), + 9: i18n.t("Modules.ET.Status.Names.RunningTime") } constructor() { this.selServerServerToken = '', this.viewStateUsers = [], this.libs = {}, - this.SrcUsrKey = -1, - this.TargetUsrKey = -1 + this.SrcUsrToken1 = '', + this.TargetUsrToken1 = '', + this.SrcUsr, + this.TargetUsr, + this.libType } - async setKey( Usr, data ){ + async getRunningTimeElapsed(){ + const now = new Date(); + let elapsedSeconds = Math.floor((now.getTime() - this.#_StartTime.getTime()) / 1000); + let elapsedStr = elapsedSeconds.toString().replaceAll('.', ''); + let hours = Math.floor(parseFloat(elapsedStr) / 3600); + elapsedSeconds = parseFloat(elapsedStr) - hours * 3600; + let minutes = Math.floor(elapsedSeconds / 60); + let seconds = elapsedSeconds - minutes * 60; + if ( hours.toString().length < 2) { hours = '0' + hours} + if ( minutes.toString().length < 2) { minutes = '0' + minutes} + if ( seconds.toString().length < 2) { seconds = '0' + seconds} + return hours + ':' + minutes + ':' + seconds + } + + async setOwnerStatus( Usr, data ){ if ( Usr == 'selSrcUsr' ){ - this.SrcUsrKey = JSONPath({path: `$..libs[0].key`, json: data}) - if ( isNaN(this.SrcUsrKey) ){ - this.SrcUsrKey = -1; - } + this.SrcUsr['isOwner'] = (JSONPath({path: `$..libs[0].key`, json: data})[0] == 0); } else { - this.TargetUsrKey = JSONPath({path: `$..libs[0].key`, json: data}) - if ( isNaN(this.TargetUsrKey) ){ - this.TargetUsrKey = -1; - } + this.TargetUsr['isOwner'] = (JSONPath({path: `$..libs[0].key`, json: data})[0] == 0); } } @@ -64,19 +80,241 @@ const viewstate = new class ViewState { return hours + ':' + minutes + ':' + seconds; } + async setLibType( libKey ){ + switch(this.libs[libKey]['type']) { + case 'movie': + this.libType = 1; + break; + case 'show': + this.libType = 4; + break; + default: + this.libType = 1; + } + } + + async getAmountOfWatched( libKey ){ + log.info(`Getting amount of watched items`); + await this.setLibType( libKey ); + let url = `${store.getters.getSelectedServerAddress}/library/sections/${libKey}/all?type=${this.libType}&lastViewedAt%3E%3E=1970-01-01&X-Plex-Container-Start=0&X-Plex-Container-Size=0`; + let header = wtutils.PMSHeader; + let totalSize; + header['X-Plex-Token'] = this.SrcUsr.token; + await axios({ + method: 'get', + url: url, + headers: header + }) + .then((response) => { + log.debug('[viewState.js] (getAmountOfWatched) Response from getAmountOfWatched recieved'); + //log.silly(`getAmountOfWatched returned as: ${JSON.stringify(response.data)}`); + totalSize = JSONPath({path: `$..totalSize`, json: response.data}); + log.debug(`[viewState.js] (getAmountOfWatched) Total Size: ${totalSize}`); + }) + .catch(function (error) { + if (error.response) { + log.error('[viewState.js] (getAmountOfWatched) getAmountOfWatched: ' + JSON.stringify(error.response.data)); + alert(error.response.data.errors[0].code + " " + error.response.data.errors[0].message); + } else if (error.request) { + log.error('[viewState.js] (getAmountOfWatched) error: ' + error.request); + } else { + log.error('[viewState.js] (getAmountOfWatched) last error: ' + error.message); + } + }); + return totalSize; + } + + async bumpViewCount( media ){ + const ratingKey = JSONPath({path: `$..ratingKey`, json: media}); + const viewCount = JSONPath({path: `$..viewCount`, json: media}); + const viewOffset = JSONPath({path: `$..viewOffset`, json: media}); + const duration = JSONPath({path: `$..duration`, json: media}); + log.info(`Bumbing viewcount to ${viewCount} for media ${ratingKey}`); + let url = `${store.getters.getSelectedServerAddress}/:/scrobble?identifier=com.plexapp.plugins.library&key=${ratingKey}`; + // We need to bump viewcount for target user same amount as for SrcUsr + let header = wtutils.PMSHeader; + header['X-Plex-Token'] = this.TargetUsr.token; + for (var i = 0; i < viewCount; i++) + { + await axios({ + method: 'get', + url: url, + headers: header + }) + .catch(function (error) { + if (error.response) { + log.error('[viewState.js] (bumpViewCount) bumpViewCount: ' + JSON.stringify(error.response.data)); + alert(error.response.data.errors[0].code + " " + error.response.data.errors[0].message); + } else if (error.request) { + log.error('[viewState.js] (bumpViewCount) error: ' + error.request); + } else { + log.error('[viewState.js] (bumpViewCount) last error: ' + error.message); + } + }); + } + // Do we need to also set an offset value? + if ( viewOffset > 0) + { + url = `${store.getters.getSelectedServerAddress}/:/timeline?ratingKey=${ratingKey}&key=%2Flibrary%2Fmetadata%2F${ratingKey}&state=stopped&time=${viewOffset}&duration=${duration}`; + await axios({ + method: 'get', + url: url, + headers: header + }) + .catch(function (error) { + if (error.response) { + log.error('[viewState.js] (bumpViewCount) viewOffset: ' + JSON.stringify(error.response.data)); + alert(error.response.data.errors[0].code + " " + error.response.data.errors[0].message); + } else if (error.request) { + log.error('[viewState.js] (bumpViewCount) viewOffset error: ' + error.request); + } else { + log.error('[viewState.js] (bumpViewCount) viewOffset last error: ' + error.message); + } + }); + } + } + + async processWatchedList( libKey ){ + log.info('[viewstate.js] (processWatchedList) Process Watched list'); + let totalSize //, size; + let start = 0; + let index = 0; + totalSize = await this.getAmountOfWatched( libKey ); + const step = wtconfig.get("PMS.ContainerSize." + this.libs[libKey]['type'], 20); + let url, gotSize; + do // Walk section in steps + { + let listProcess = {}; + url = `${store.getters.getSelectedServerAddress}/library/sections/${libKey}/all?type=${this.libType}&lastViewedAt%3E%3E=1970-01-01&X-Plex-Container-Start=${start}&X-Plex-Container-Size=${step}&excludeElements=Genre,Director,Writer,Country,Role,Producer,Collections,Media&excludeFields=summary,tagline,rating,contentRating,audienceRatingImage,file`; + // Now go grab the medias + let header = wtutils.PMSHeader; + header['X-Plex-Token'] = this.SrcUsr.token; + await axios({ + method: 'get', + url: url, + headers: header + }) + .then((response) => { + log.debug('[viewState.js] (processWatchedList) Response from processWatchedList recieved'); + log.silly(`processWatchedList returned as: ${JSON.stringify(response.data)}`); + gotSize = JSONPath({path: `$.MediaContainer.size`, json: response.data})[0]; + const medias = JSONPath({path: `$..Metadata`, json: response.data})[0]; + for (var media in medias){ + let listProcessDetails = {}; + listProcessDetails['title'] = JSONPath({path: `$..title`, json: medias[media]})[0]; + listProcessDetails['viewOffset'] = JSONPath({path: `$..viewOffset`, json: medias[media]})[0]; + listProcessDetails['lastViewedAt'] = JSONPath({path: `$..lastViewedAt`, json: medias[media]})[0]; + listProcessDetails['viewCount'] = JSONPath({path: `$..viewCount`, json: medias[media]})[0]; + listProcessDetails['duration'] = JSONPath({path: `$..duration`, json: medias[media]})[0]; + listProcessDetails['ratingKey'] = JSONPath({path: `$..ratingKey`, json: medias[media]})[0]; + listProcess[JSONPath({path: `$..ratingKey`, json: medias[media]})[0]] = listProcessDetails; + index += 1; + this.bumpViewCount( listProcessDetails ); + this.updateStatusMsg(5, i18n.t("Modules.PMS.ViewState.Status.Msg.ProcessItem1", [listProcessDetails['title'], index, totalSize])); + } + }) + .catch(function (error) { + if (error.response) { + log.error('[viewState.js] (processWatchedList) processWatchedList: ' + JSON.stringify(error.response.data)); + alert(error.response.data.errors[0].code + " " + error.response.data.errors[0].message); + } else if (error.request) { + log.error('[viewState.js] (processWatchedList) error: ' + error.request); + } else { + log.error('[viewState.js] (processWatchedList) last error: ' + error.message); + } + }); + start += step; + } while ( gotSize == step ); + } + + async walkSourceUsr(){ + log.info('[viewstate.js] (walkSourceUsr) Walking SourceUsr'); + var keyCount = Object.keys(this.libs).length; + let index = 1; + for (var libKey in this.libs){ + this.updateStatusMsg(4, i18n.t("Modules.PMS.ViewState.Status.Msg.Processing2", [index, keyCount, this.libs[libKey]['title']])); + await this.processWatchedList( libKey ); + index += 1; + } + } + + async getUsrTokens(){ + log.info('[viewstate.js] Starting getUsrTokens'); + // Ask for a list of devices + let header = wtutils.PMSHeader; + // Add server token + header['X-Plex-Token'] = this.selServerServerToken[0]; + // Remove unwanted headers + delete header['X-Plex-Client-Identifier'] + delete header['X-Plex-Device'] + delete header['X-Plex-Product'] + delete header['X-Plex-Version'] + const url = `${wtutils.plexTVApi}v2/server/access_tokens`; + await axios({ + method: 'get', + url: url, + headers: header + }) + .then((response) => { + log.debug('[viewState.js] Response from getUsrTokens recieved'); + //log.silly(`getUsrTokens returned as: ${JSON.stringify(response.data)}`); + var users = JSONPath({path: `$..[?(@.type == 'server')]`, json: response.data}); + for (var user in users){ + var userInfo = users[user] + if ( userInfo['invited']['id'] == this.SrcUsr['id']){ + if ( store.getters.getUsers[userInfo['invited']['id']]['friendlyName'] ){ + this.SrcUsr['title'] = store.getters.getUsers[userInfo['invited']['id']]['friendlyName']; + } + else { + this.SrcUsr['title'] = userInfo['invited']['title']; + } + this.SrcUsr['token'] = userInfo['token']; + } + else if ( userInfo['invited']['id'] == this.TargetUsr['id']){ + if ( store.getters.getUsers[userInfo['invited']['id']]['friendlyName'] ){ + this.TargetUsr['title'] = store.getters.getUsers[userInfo['invited']['id']]['friendlyName']; + } + else { + this.TargetUsr['title'] = userInfo['invited']['title']; + } + this.TargetUsr['token'] = userInfo['token']; + } + } + if (this.SrcUsr.isOwner){ + this.SrcUsr['token'] = store.getters.getAuthToken; + this.SrcUsr['title'] = store.getters.getPlexName; + this.SrcUsr['id'] = store.getters.getMeId; + } + if (this.TargetUsr.isOwner){ + this.TargetUsr['token'] = store.getters.getAuthToken; + this.TargetUsr['title'] = store.getters.getPlexName; + this.TargetUsr['id'] = store.getters.getMeId; + } + }) + .catch(function (error) { + if (error.response) { + log.error('[viewState.js] getUsrTokens: ' + JSON.stringify(error.response.data)); + alert(error.response.data.errors[0].code + " " + error.response.data.errors[0].message); + } else if (error.request) { + log.error('[viewState.js] getUsrTokens: ' + error.request); + } else { + log.error('[viewState.js] getUsrTokens: ' + error.message); + } + }); + + } + async copyViewState( SrcUsr, TargetUsr ){ log.info('[viewstate.js] Starting copyViewState'); const startTime = await this.getNowTime('start'); - console.log('Ged 4 start time: ' + startTime) - this.updateStatusMsg(3, startTime); - - - console.log('Ged 1 SrcUsr: ' + JSON.stringify(SrcUsr)) - console.log('Ged 2 TargetUsr: ' + JSON.stringify(TargetUsr)) - - this.updateStatusMsg(1, i18n.t("Modules.PMS.ViewState.Status.Msg.Processing")); + //this.updateStatusMsg( this.RawMsgType.TimeElapsed, await this.getRunningTimeElapsed()); await this.getLibs( SrcUsr, TargetUsr ); - this.updateStatusMsg(1, "Ged") + //this.updateStatusMsg(3, startTime); + startTime + this.updateStatusMsg(1, i18n.t("Modules.PMS.ViewState.Status.Msg.Processing")); + await this.getUsrTokens(); + await this.walkSourceUsr(); + this.updateStatusMsg(1, i18n.t("Modules.PMS.ViewState.Status.Msg.Idle")); } // Update status msg @@ -94,7 +332,6 @@ const viewstate = new class ViewState { } }) store.commit("UPDATE_viewStateStatus", newMsg); - console.log('Ged 7 Msg: ' + JSON.stringify(newMsg)) } // Clear Status Window @@ -106,9 +343,12 @@ const viewstate = new class ViewState { } async getLibs( SrcUsr, TargetUsr ){ - log.info('[viewstate.js] Starting getLibs'); + log.info('[viewstate.js] (getLibs) Starting getLibs'); + log.silly(`[viewstate.js] (getLibs) SrcUsr: ${JSON.stringify(SrcUsr)}`); + log.silly(`[viewstate.js] (getLibs) TargetUsr: ${JSON.stringify(TargetUsr)}`); // Are both users selected ? - if ( this.TargetUsrKey < 0 || this.SrcUsrKey < 0 ){ + if ( !(this.TargetUsr && this.SrcUsr) ){ + log.info('[viewstate.js] (getLibs) Both users not yet selected, so exit'); return; } if ( JSON.stringify(SrcUsr) === JSON.stringify(TargetUsr) ){ @@ -119,15 +359,18 @@ const viewstate = new class ViewState { } this.clearStatus(); this.updateStatusMsg(1, i18n.t("Modules.PMS.ViewState.Status.Msg.GatheringLibs")); - log.silly(`[viewstate.js] Source usr: ${JSON.stringify(SrcUsr)}`); - log.silly(`[viewstate.js] Target usr: ${JSON.stringify(TargetUsr)}`); + log.silly(`[viewstate.js] (getLibs) Source usr: ${JSON.stringify(SrcUsr)}`); + log.silly(`[viewstate.js] (getLibs) Target usr: ${JSON.stringify(TargetUsr)}`); this.libs = {}; if ( JSONPath({path: `$..libs[0].key`, json: SrcUsr}) == 0 ) { // We need to add all libs from target usr log.debug(`[viewstate.js] SrcUsr is owner`); for(let i of TargetUsr["libs"]) { - this.libs[i.key] = i.title; + let entry = {}; + entry['title'] = i.title; + entry['type'] = i.type; + this.libs[i.key] = entry; } } else @@ -136,21 +379,27 @@ const viewstate = new class ViewState { // We need to add all libs from Source usr log.debug(`[viewstate.js] TargetUsr is owner`); for(let i of SrcUsr["libs"]) { - this.libs[i.key] = i.title; + let entry = {}; + entry['title'] = i.title; + entry['type'] = i.type; + this.libs[i.key] = entry; } } else { for(let i of SrcUsr["libs"]) { if ( JSON.stringify(TargetUsr["libs"]).indexOf(JSON.stringify(i)) > -1) { - this.libs[i.key] = i.title; + let entry = {}; + entry['title'] = i.title; + entry['type'] = i.type; + this.libs[i.key] = entry; } } } } let libstatus = [] for (let lib in this.libs){ - libstatus.push(this.libs[lib]) + libstatus.push(this.libs[lib]['title']) } this.updateStatusMsg(1, i18n.t("Modules.PMS.ViewState.Status.Msg.Idle")); this.updateStatusMsg(2, libstatus.join(', ')) @@ -171,9 +420,8 @@ const viewstate = new class ViewState { }) .then((response) => { log.debug('[viewState.js] Response from getServerToken recieved'); - //log.silly(`getServerToken returned as: ${JSON.stringify(response.data)}`); this.selServerServerToken = JSONPath({path: `$[?(@.clientIdentifier== '${clientIdentifier}')].token`, json: response.data}); - log.silly(`[viewState.js] selServerServerToken returned as: ${this.selServerServerToken}`); + log.silly(`[viewState.js] selServerServerToken returned ok`); }) .catch(function (error) { if (error.response) { diff --git a/src/store/modules/plextv.js b/src/store/modules/plextv.js index 4035dcf..081da1d 100644 --- a/src/store/modules/plextv.js +++ b/src/store/modules/plextv.js @@ -155,7 +155,7 @@ const actions = { commit('UPDATE_AUTHENTICATED', true) commit('UPDATE_AVATAR', response.data.thumb) commit('UPDATE_PLEXNAME', response.data.username) - commit('UPDATE_MeId', response.data.id) + commit('UPDATE_MeId', response.data.user.id) router.replace({name: "home"}); }) .catch(function (error) { @@ -199,13 +199,11 @@ const actions = { }) .then(function (response) { log.debug('loginToPlex: Response from fetchPlexServers recieved') - - console.log('Gedd 66 Me: ' + JSON.stringify(response.data.user)) commit('UPDATE_AUTHTOKEN', response.data.user.authToken) commit('UPDATE_AUTHENTICATED', true) commit('UPDATE_AVATAR', response.data.user.thumb) commit('UPDATE_PLEXNAME', response.data.user.username) - commit('UPDATE_MeId', response.data.id) + commit('UPDATE_MeId', response.data.user.id) router.replace({name: "home"}); }) .catch(function (error) {