Skip to content

Commit

Permalink
Inital commit
Browse files Browse the repository at this point in the history
  • Loading branch information
lart2150 committed Aug 10, 2021
0 parents commit af9c624
Show file tree
Hide file tree
Showing 7 changed files with 349 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/config.js
/cdata.*
129 changes: 129 additions & 0 deletions autoDelete.js
Original file line number Diff line number Diff line change
@@ -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();
13 changes: 13 additions & 0 deletions config.js.dist
Original file line number Diff line number Diff line change
@@ -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.
};
148 changes: 148 additions & 0 deletions lib/tivo.js
Original file line number Diff line number Diff line change
@@ -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
}
}
3 changes: 3 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
}
42 changes: 42 additions & 0 deletions reboot.js
Original file line number Diff line number Diff line change
@@ -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();

0 comments on commit af9c624

Please sign in to comment.