From af9c62499716650c9e3738af50ec0483fd21f681 Mon Sep 17 00:00:00 2001 From: Brian Engert Date: Mon, 9 Aug 2021 19:30:31 -0500 Subject: [PATCH] Inital commit --- .gitignore | 2 + autoDelete.js | 129 ++++++++++++++++++++++++++++++++++++++++ config.js.dist | 13 ++++ lib/tivo.js | 148 ++++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 3 + package.json | 12 ++++ reboot.js | 42 +++++++++++++ 7 files changed, 349 insertions(+) create mode 100644 .gitignore create mode 100644 autoDelete.js create mode 100644 config.js.dist create mode 100644 lib/tivo.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 reboot.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a81352 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/config.js +/cdata.* \ No newline at end of file diff --git a/autoDelete.js b/autoDelete.js new file mode 100644 index 0000000..50f9a7a --- /dev/null +++ b/autoDelete.js @@ -0,0 +1,129 @@ +import {Tivo} from './lib/tivo.js' +import {config} from './config.js'; + +/** + * + * @param {{title: string, collectionId: string}} recording + * @returns shouldDelete + */ +const shouldDelete = (recording) => { + if ( + (config.deleteTitles && config.deleteTitles.includes(recording.title)) || + (config.deleteRegex && config.deleteRegex.test(recording.title)) + ) { + return true; + } + return false; +} + +const doit = async () => { + const tivo = new Tivo(config.ip, config.mak); + await tivo.connect(); + console.log('connected'); + + let recordings = []; + if (config.deleteScanAll || config.deleteDuplicats) { + const body = await tivo.sendRequestAllPages( + 'recordingSearch', + {"state":["deleted"]}, + 'recording' + ); + recordings = body.recording; + } else { + const body = await tivo.sendRequest( + 'recordingSearch', + { + state: ['deleted'], + count: 50 + } + ); + recordings = body.recording; + } + + + for (let i = 0; i < recordings.length; i++) { + const r = recordings[i]; + //console.log(r.title, r.subtitle, r.collectionId); + if (shouldDelete(r)) { + console.log(r.title, r.subtitle, r.collectionId); + console.log('DELETE'); + const deleted = await tivo.sendRequest('recordingUpdate', {state:"contentDeleted", recordingId:[r.recordingId]}); + // const deleted = {}; + if (deleted.type === 'success') { + console.log('done did deleted that thing'); + } else { + console.log('error deleting show?', deleted); + } + } + } + + if (config.deleteDuplicats && recordings.length) { + const shows = new Map(); + recordings.forEach(r => { + if (!r.seasonNumber || !r.episodeNum || !r.subtitle) { + return; + } + if (!shows.get(r.title)) { + const show = [r] + shows.set(r.title, show); + return; + } + shows.get(r.title).push(r); + }); + //console.log(shows); + + for (const [title, showRecordings] of shows.entries()) { + + //console.log(title) + for (const r of showRecordings) { + const dupes = showRecordings.filter( + b => r.seasonNumber === b.seasonNumber + && r.episodeNum[0] === b.episodeNum[0] + && r.subtitle === b.subtitle + ); + + if (dupes.length > 1 ){ + dupes.sort((a, b) => { + //pick hd over sd + if (a.hdtv !== b.hdtv) { + return !a.hdtv + } + + //one has skip mode the other does not pick the one with + //this might need to be updated to look for "segmentType": "adSkip" + if (b.clipMetadata === undefined && a.clipMetadata) { + return -1 + } + if (a.clipMetadata === undefined && b.clipMetadata) { + return 1 + } + + return Math.sign(b.duration - a.duration); + }) + + //keep the first recording + for (let i = 1; i < dupes.length; i++) { + const r = dupes[i]; + console.log(`Deleting Dupe: ${r.title} S${r.seasonNumber}E${r.episodeNum[0]} - ${r.subtitle} ${r.startTime}`); + const deleted = await tivo.sendRequest('recordingUpdate', {/*"bodyId": "tsn:84900019045ED87",*/state:"contentDeleted", recordingId:[r.recordingId]}); + //const deleted = {type:'success'}; + if (deleted.type === 'success') { + console.log('done did deleted that thing'); + const index = showRecordings.findIndex(rec => rec.recordingId === r.recordingId); + showRecordings.splice(index, 1); + } else { + console.log('error deleting show?', deleted); + } + } + } + } + } + } + + + + //console.log(body); + tivo.disconnect(); +} + +doit(); \ No newline at end of file diff --git a/config.js.dist b/config.js.dist new file mode 100644 index 0000000..da91cc4 --- /dev/null +++ b/config.js.dist @@ -0,0 +1,13 @@ +export const config = { + ip: '192.168.1.1', + mak: '1234567890', + deleteTitles: [ + 'The Late Show With Stephen Colbert', + 'The Daily Show With Trevor Noah', + 'CBS Evening News With Norah O\'Donnell', + 'CBS Weekend News', + ], + deleteRegex: /^Manual: /, + deleteScanAll: false,//false it will only check the 50 most recently deleted recordings. false is way faster if you have a lot of deleted recordings + deleteDuplicats: false, //setting this to true will flip deleteScanAll to true. +}; \ No newline at end of file diff --git a/lib/tivo.js b/lib/tivo.js new file mode 100644 index 0000000..f6173f5 --- /dev/null +++ b/lib/tivo.js @@ -0,0 +1,148 @@ +'use strict' +import tls from 'tls'; +import fs from 'fs'; + +export class Tivo { + /** + * + * @param {string} ip Tivo ip address + * @param {string} mak media access key + */ + constructor(ip, mak) { + this.rpcId = 0; + this.ip = ip; + this.mak = mak; + } + + async connect () { + const options = { + host : this.ip, + rejectUnauthorized: false, + port : 1413, + pfx : fs.readFileSync('cdata.p12'), + passphrase : fs.readFileSync('cdata.password'), + }; + + this.sessionID = Math.floor(Math.random() * 72736 + 2539520).toString(16); + + const promise = new Promise((resolve, reject) => { + this.received = resolve; + this.socket = tls.connect(options, () => { + this.socket.setEncoding('utf8'); + this.socket.write(this.buildRequest("bodyAuthenticate", {"type":"bodyAuthenticate","credential":{"type":"makCredential","key":this.mak}})); + + this.data = ""; + + this.socket.on('data', this.read.bind(this)); + + this.socket.on('error', (err) => { + console.error('TIVO TLS error', err); + this.socket.end(); + }); + }); + }); + + const response = await promise; + this.received = null; + + const bodyResponse = await this.sendRequest('bodyConfigSearch', {"bodyId": "-"}); + this.bodyId = bodyResponse.bodyConfig[0].bodyId; + } + + async sendRequestAllPages(type, content, responseKey, count = 50) { + content.count = 50; + let response = await this.sendRequest(type, content); + let combinedResponse = response; + let offset = count; + while (response.isBottom === false) { + content.offset = offset; + console.log(offset); + offset += count; + response = await this.sendRequest(type, content); + combinedResponse[responseKey] = combinedResponse[responseKey].concat(response[responseKey]); + } + return combinedResponse; + } + + async sendRequest(type, content) { + if (this.received) { + await this.received; + } + + this.bodyLength = null; + this.body = null; + this.data = null; + + const request = this.buildRequest(type, content); + //console.log(request); + const promise = new Promise((resolve, reject) => { + this.received = resolve; + this.socket.write(request) + }); + + const response = await promise; + this.received = null; + //console.log(response); + const responseBody = JSON.parse(response); + + if (responseBody.type === 'error') { + throw new Error(response); + } + + return responseBody; + } + + disconnect() { + this.socket.end(); + } + + /** + * used for callbaks don't use + * @param {string} chunk + */ + read(chunk) { + if (chunk.indexOf('MRPC/2 ') === 0) { + const header = chunk.split("\r\n")[0]; + this.bodyLength = parseInt(header.split(" ")[2], 10); + this.body = chunk.split("\r\n\r\n")[1]; + this.chunkCount = 1; + } else if (this.bodyLength) { + this.chunkCount++; + this.body += chunk; + } + this.data += chunk; + // I don't know why this.body.length does not match this.bodyLength some times. this can lead to a hang as the promise will never resolve. + if (this.received && this.body && this.body.length >= this.bodyLength - this.chunkCount) { + this.received(this.body); + } + } + + + /** + * + * @param {string} type + * @param {object} content + * @returns {string} + */ + buildRequest(type, content) { + content.type = type; + if (!content.bodyId && this.bodyId) { + content.bodyId = this.bodyId; + } + const eol = "\r\n"; + const header = "Type: request" + eol + + `RpcId: ${this.rpcId++}` + eol + + "SchemaVersion: 17" + eol + + "Content-Type: application/json" + eol + + `RequestType: ${type}` + eol + + "ResponseCount: single" + eol + + "BodyId: " + eol + + "X-ApplicationName: Quicksilver" + eol + + "X-ApplicationVersion: 1.2" + eol + + "X-ApplicationSessionId: 0x" + this.sessionID + eol + eol; + + const body = JSON.stringify(content) + "\n" + + return "MRPC/2 " + header.length + " " + body.length + eol + header + body + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..48e341a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3 @@ +{ + "lockfileVersion": 1 +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6fef7f2 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "tivo-node", + "version": "0.1.0", + "description": "", + "main": "autoDelete.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "type":"module", + "license": "GPL" +} diff --git a/reboot.js b/reboot.js new file mode 100644 index 0000000..c92f24d --- /dev/null +++ b/reboot.js @@ -0,0 +1,42 @@ +import {Tivo} from './lib/tivo.js' +import {config} from './config.js'; + +const doit = async () => { + const tivo = new Tivo(config.ip, config.mak); + await tivo.connect(); + console.log('connected'); + + let body = await tivo.sendRequest( + 'uiNavigate', + { + uri: 'x-tivo:classicui:restartDvr' + } + ); + + await new Promise(resolve => setTimeout(resolve, 5000)); + + body = await tivo.sendRequest( + 'keyEventSend', + {event: 'thumbsDown'} + ); + console.log(body); + body = await tivo.sendRequest( + 'keyEventSend', + {event: 'thumbsDown'} + ); + body = await tivo.sendRequest( + 'keyEventSend', + {event: 'thumbsDown'} + ); + + body = await tivo.sendRequest( + 'keyEventSend', + {event: 'enter'} + ); + + + //console.log(body); + tivo.disconnect(); +} + +doit(); \ No newline at end of file