-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit af9c624
Showing
7 changed files
with
349 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
/config.js | ||
/cdata.* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |