Skip to content

Commit

Permalink
Initial release.
Browse files Browse the repository at this point in the history
  • Loading branch information
Matthew Sanders committed Mar 12, 2022
1 parent 6249cf2 commit 3fe283b
Show file tree
Hide file tree
Showing 20 changed files with 4,897 additions and 1 deletion.
16 changes: 16 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# EditorConfig is awesome: https://EditorConfig.org

# top-most EditorConfig file
root = true

[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = false

[*.{css,cjs,js,jsx,ts,tsx,mjs,json,jsonc}]
indent_size = 2
insert_final_newline = true
18 changes: 18 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module.exports = {
env: {
es2021: true,
node: true,
},
extends: [
'airbnb-base',
],
parserOptions: {
ecmaVersion: 12,
sourceType: 'module',
},
rules: {
'max-len': 'off',
'no-console': 'off',
'import/extensions': 'off',
},
};
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ bower_components

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
build/

# Dependency directories
node_modules/
Expand Down Expand Up @@ -102,3 +103,9 @@ dist

# TernJS port file
.tern-port

# The results of running the script
output/

# The source data
src/Takeout/
3 changes: 3 additions & 0 deletions .stylelintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "stylelint-config-standard"
}
73 changes: 72 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,72 @@
# google-voice-transformer
# google-voice-transformer

This application transforms your Google Voice text messages to much more readable formats.

## Features

* Combines all messages, per user, into single files so you can _finally_ search through your messages.
* Correctly displays media files instead of simply presenting the user with a link to the media file.
* Converts all video files to mp4.
* Groups all messages by the full date.
* Fixes the references to media files in your messages. Google does not include the file extension in the anchor tag. Google also references broken filenames due to having truncated long filenames in the html source.

## Screenshots

<a href="https://user-images.githubusercontent.com/4317724/158003471-2fba9d7f-cd5c-4c8c-97d3-96919a28b609.png" title="default template">
<img src="https://user-images.githubusercontent.com/4317724/158003471-2fba9d7f-cd5c-4c8c-97d3-96919a28b609.png" width="350">
</a> <a href="https://user-images.githubusercontent.com/4317724/158003476-3d5376f6-8ebb-4ee5-8003-c7c33ba2c041.png" title="WhatsApp template">
<img src="https://user-images.githubusercontent.com/4317724/158003476-3d5376f6-8ebb-4ee5-8003-c7c33ba2c041.png" width="350">
</a>

## Requirements

Your [Google Takeout][1] data. Make sure that it includes your Google Voice data. Extract the archive and save the `Takeout` directory to disk.

![Test Image 3](docs/images/takeout.png)

## Usage

```posh
Usage: google-voice-transformer [options]
Convert the Google Takeout format to a more readable format.
Options:
-V, --version output the version number
--path <string> (optional) The path to your Takeout folder. The program will look for this folder in the same
directory
--template <type> (optional) The output file template (choices: "default", "whatsapp")
-h, --help display help for command
```

This application is ran from the command line. The options are optional. An `output` folder will be created in the directory this application is ran.

`--path`: if you leave this empty the application will default to looking for your `Takeout` folder in the same directory from which this application was ran.

`--template`: if you leave this empty the application will use a style similar to Google Voice.

### Command line examples

_Default template with the Takeout folder in the same directory._

```posh
.\gvt-win.exe
```

_WhatsApp template with the Takeout folder in the same directory._

```posh
.\gvt-win.exe --template whatsapp
```

_WhatsApp template with the Takeout folder on another drive._

```posh
.\gvt-win.exe --template whatsapp --path D:\Takeout
```

## Issues

* If you have messages that are more than a decade old it is normal to see a warning on the command line about missing media files. This is due to Google not including your very old media files into the Takeout archive.

[1]: https://takeout.google.com/settings/takeout "Google Takeout"
Binary file added docs/images/takeout.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "google-voice-transformer",
"version": "0.1.0",
"description": "Convert Google Voice files from Google Takeout to be more readable",
"main": "app.js",
"license": "GPL-3.0-or-later",
"homepage": "https://github.com/Clearmist/google-voice-transformer",
"repository": {
"type": "git",
"url": "https://github.com/Clearmist/google-voice-transformer"
},
"type": "module",
"scripts": {
"build": "webpack && pkg build/app.js -o build/Release/gvt -t linux,macos,win",
"run": "node src/app.js",
"run-whatsapp": "node src/app.js --template whatsapp"
},
"pkg": {
"outputPath": "build"
},
"dependencies": {
"commander": "^9.0.0",
"cross-env": "^7.0.3",
"fast-glob": "^3.2.11",
"handbrake-js": "^6.0.0",
"node-html-parser": "^5.2.0",
"webpack": "^5.70.0"
},
"devDependencies": {
"css-loader": "^6.7.1",
"eslint": "^8.10.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.25.4",
"html-loader": "^3.1.0",
"pkg": "^5.5.2",
"stylelint": "^14.5.3",
"stylelint-config-standard": "^25.0.0",
"to-string-loader": "^1.2.0",
"webpack-cli": "^4.9.2"
}
}
13 changes: 13 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import fg from 'fast-glob';
import { globOptions, options } from './library/options.js';
import { createFolder } from './library/output.js';
import parseSourceFiles from './library/parse.js';

// Get a list of text messages.
const textSources = fg.sync([`${options.callsPath}/* - Text - *.{htm,html}`], globOptions);

if (Array.isArray(textSources)) {
createFolder('output/media');

parseSourceFiles(textSources);
}
9 changes: 9 additions & 0 deletions src/library/components.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const components = {
whatsapp: {
metadata: `<span class="metadata">
<span class="time">{{time}}</span>
</span>`,
},
};

export default components;
32 changes: 32 additions & 0 deletions src/library/convert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import fs from 'fs';
import path from 'path';
import hbjs from 'handbrake-js';

export default function convert(details) {
return new Promise((resolve, reject) => {
const extension = path.extname(details.fullPath);
const relativePath = `${details.relativePath.replace(extension, '')}.mp4`;
const output = path.join(process.cwd(), 'output', relativePath);

const options = {
input: details.fullPath,
output,
};

if (fs.existsSync(output)) {
console.log(`Skipping existing video file: ${details.filename}`);

resolve();
} else {
console.log(`Converting video file: ${details.filename}`);

hbjs.exec(options, (err, stdout, stderr) => {
if (err) {
console.error(stderr);
}

resolve();
});
}
});
}
120 changes: 120 additions & 0 deletions src/library/format.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import path from 'path';
import { options } from './options.js';
import components from './components.js';

const months = {
abbreviated: {
0: 'Jan',
1: 'Feb',
2: 'Mar',
3: 'Apr',
4: 'May',
5: 'Jun',
6: 'Jul',
7: 'Aug',
8: 'Sep',
9: 'Oct',
10: 'Nov',
11: 'Dec',
},
full: {
0: 'January',
1: 'February',
2: 'March',
3: 'April',
4: 'May',
5: 'June',
6: 'July',
7: 'August',
8: 'September',
9: 'October',
10: 'November',
11: 'December',
},
};

export function formatTime(timestamp) {
const date = new Date(timestamp);
const hours24 = date.getHours();
const hours12 = hours24 > 12 ? hours24 - 12 : hours24;
const meridian = hours24 > 11 ? 'PM' : 'AM';

let timeString = date.toLocaleTimeString();

if (options.template === 'whatsapp') {
timeString = `${hours12}:${date.getMinutes()} ${meridian}`;
} else {
const left = `${months.abbreviated[date.getMonth()]} ${date.getDate()}`;
const right = `${hours12}:${date.getMinutes()} ${meridian}`;

timeString = `<span title="${left} ${date.getFullYear()}, ${right}">
${left}, ${right}
</span>`;
}

return timeString;
}

export function formatSeparator(previousTimestamp, timestamp) {
let separator = false;

const previousDate = new Date(previousTimestamp);
const date = new Date(timestamp);

if (previousTimestamp === '' || previousDate.toDateString() !== date.toDateString()) {
separator = `${months.full[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`;
}

return separator;
}

function formatMetadata(message) {
let metadata = '';

if (message.type !== 'separator') {
if (options.template === 'whatsapp') {
metadata = components.whatsapp.metadata.replace('{{time}}', message.metadata.time);
} else {
metadata = `</div><div class="status ${message.direction}">${message.metadata.time}`;
}
}

return metadata;
}

export function formatMessage(message) {
let metadata = formatMetadata(message);
let html = message.messageText;
let divClass = 'message';

if (message.type === 'audio') {
html = `<div class="media audio">
<audio controls src="${message.messageText.relativePath}">
</div>`;
} else if (message.type === 'image') {
html = `<div class="media image">
<a target="_blank" href="${message.messageText.relativePath}">
<img src="${message.messageText.relativePath}" />
</a>
</div>`;
metadata = '';
} else if (message.type === 'vcard') {
html = `<a href="${message.messageText.relativePath}">Contact card</a>`;
} else if (message.type === 'video') {
// All video is either already mp4 or has been converted to mp4.
const relativePath = message.messageText.relativePath.replace(path.extname(message.messageText.relativePath), '.mp4');

html = `<div class="media video">
<video controls>
<source src="${relativePath}" type="video/mp4">
Sorry, your browser doesn't support embedded videos.
</video>
</div>`;
} else if (message.type === 'separator') {
divClass = '';
}

return `\n<div class="${divClass} ${message.direction}">
${html}${metadata}
</div>`;
}
44 changes: 44 additions & 0 deletions src/library/options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import fs from 'fs';
import path from 'path';
import { Command, Option } from 'commander';

export const program = new Command();

program
.name('google-voice-transformer')
.description('CLI to convert Google Takeout (Voice) to a more readable format')
.version('0.1.0');

program
.description('Convert the Google Takeout format to a more readable format.')
.option('--path <string>', '(optional) The path to your Takeout folder. The program will look for this folder in the same directory')
.addOption(new Option('--template <type>', '(optional) The output file template').choices(['default', 'whatsapp']));

export const options = {};

program.parse();

const userInput = program.opts();

// Clean the template option.
userInput.template = userInput.template ?? 'default';
options.template = userInput.template.toLowerCase();

// Verify that the path to the data directory exists.
options.path = path.normalize(userInput.path ?? path.join(process.cwd(), 'Takeout')).replaceAll('\\', '/');

if (fs.existsSync(options.path) === false) {
program.error('The path to the Takeout folder does not exist. Either run this program from the same place where you are storing your Takeout directory or use the --path <fullpath> option to point the program to the location of your Takeout directory.');
}

options.callsPath = path.join(options.path, 'Voice', 'Calls').replaceAll('\\', '/');

// Verify that the Calls sub-directory exists in the Takeout folder.
if (fs.existsSync(options.callsPath) === false) {
program.error('Your Takeout folder does not contain the ./Takeout/Voice/Calls subdirectory.');
}

export const globOptions = {
dot: false,
onlyFiles: true,
};
Loading

0 comments on commit 3fe283b

Please sign in to comment.