Files
homebrew-magic-rpg/Foundry-Data/gdrive-client.js
T
Octoturge a4f8941fca chore: initialize homebrew-magic-rpg monorepo
Consolidates the HbM: RPG project (formerly the loose hbm-books
working folder) into a single repo. ObsidianNotes, foundry-lore, and
foundry-system are wired in as submodules pointing at their existing
Gitea repos. Loose tooling scripts are organized under tools/,
obsolete/one-off scripts (fix_vault.py, generate_mocs.py, debug-*.ts,
write_file_helper.py) and _books/ (superseded by ObsidianNotes) are
excluded via .gitignore rather than deleted. foundryvtt-admin is
excluded pending its own separate repo.
2026-08-17 21:28:50 +02:00

262 lines
7.1 KiB
JavaScript

/**
* HbM: RPG v3 - Google Drive Client
*
* Downloads and processes books from Google Drive for content creation
*/
import { google } from 'googleapis';
import fs from 'fs/promises';
import path from 'path';
const SCOPES = [
'https://www.googleapis.com/auth/drive.readonly',
'https://www.googleapis.com/auth/documents.readonly'
];
// Root of the vault (one level above .src/)
const ROOT_DIR = path.resolve(import.meta.dirname, '..');
const TOKEN_PATH = path.join(ROOT_DIR, 'gdrive-token.json');
const CREDENTIALS_PATH = path.join(import.meta.dirname, 'client_secret_837588858675-hp2m36p432m755aeuudtigo1225388i7.apps.googleusercontent.com.json');
// Book IDs will be stored here after first search
const BOOKS_CACHE_PATH = path.join(import.meta.dirname, 'data', 'books-cache.json');
export class GDriveClient {
constructor() {
this.auth = null;
this.drive = null;
this.docs = null;
}
/**
* Initialize OAuth client only (without requiring token)
*/
async initOAuth() {
const credentials = JSON.parse(await fs.readFile(CREDENTIALS_PATH, 'utf-8'));
const { client_id, client_secret, redirect_uris } = credentials.installed || credentials.web;
this.auth = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
return this;
}
async init() {
await this.initOAuth();
// Try to load existing token
try {
const token = JSON.parse(await fs.readFile(TOKEN_PATH, 'utf-8'));
this.auth.setCredentials(token);
} catch (err) {
await this.authenticate();
}
this.drive = google.drive({ version: 'v3', auth: this.auth });
this.docs = google.docs({ version: 'v1', auth: this.auth });
return this;
}
async authenticate() {
const authUrl = this.auth.generateAuthUrl({
access_type: 'offline',
scope: SCOPES,
});
console.log('\n🔐 Authorization required!');
console.log('Please visit this URL to authorize:\n');
console.log(authUrl);
console.log('\nThen run: node src/gdrive-client.js auth <CODE>\n');
process.exit(1);
}
async saveToken(code) {
const { tokens } = await this.auth.getToken(code);
this.auth.setCredentials(tokens);
await fs.writeFile(TOKEN_PATH, JSON.stringify(tokens, null, 2));
console.log('✅ Token saved successfully!');
}
/**
* Search for all HbM: RPG v3 books
*/
async searchBooks() {
const query = "name contains 'HbM: RPG v3' and mimeType = 'application/vnd.google-apps.document' and trashed = false";
const res = await this.drive.files.list({
q: query,
fields: 'files(id, name, modifiedTime, webViewLink, ownedByMe)',
orderBy: 'name',
includeItemsFromAllDrives: false,
supportsAllDrives: false,
});
return res.data.files;
}
/**
* Get document content as plain text
*/
async getDocumentAsText(fileId) {
const res = await this.drive.files.export({
fileId: fileId,
mimeType: 'text/plain',
});
return res.data;
}
/**
* Get document content as HTML (preserves some formatting)
*/
async getDocumentAsHtml(fileId) {
const res = await this.drive.files.export({
fileId: fileId,
mimeType: 'text/html',
});
return res.data;
}
/**
* Get document structure using Docs API (best for structured editing)
*/
async getDocumentStructure(documentId) {
const res = await this.docs.documents.get({
documentId: documentId,
});
return res.data;
}
/**
* Download a specific book by name pattern
*/
async downloadBook(namePattern) {
const books = await this.searchBooks();
const book = books.find(b => b.name.toLowerCase().includes(namePattern.toLowerCase()));
if (!book) {
throw new Error(`Book not found: ${namePattern}`);
}
console.log(`📖 Downloading: ${book.name}`);
const text = await this.getDocumentAsText(book.id);
const html = await this.getDocumentAsHtml(book.id);
return {
id: book.id,
name: book.name,
modifiedTime: book.modifiedTime,
text,
html,
};
}
/**
* Download all books and save to local cache
*/
async downloadAllBooks(outputDir = '_books') {
await fs.mkdir(outputDir, { recursive: true });
const books = await this.searchBooks();
// Deduplicate by name (keep the most recently modified)
const uniqueBooks = new Map();
for (const book of books) {
const existing = uniqueBooks.get(book.name);
if (!existing || new Date(book.modifiedTime) > new Date(existing.modifiedTime)) {
uniqueBooks.set(book.name, book);
}
}
const results = [];
for (const book of uniqueBooks.values()) {
// Skip character sheets
if (book.name.includes('KP v')) continue;
console.log(`📖 Downloading: ${book.name}`);
try {
const text = await this.getDocumentAsText(book.id);
const safeName = book.name.replace(/[^a-zA-Z0-9ąćęłńóśźżĄĆĘŁŃÓŚŹŻ\-\s]/g, '').trim();
// Save as markdown
const mdPath = path.join(outputDir, `${safeName}.md`);
await fs.writeFile(mdPath, `# ${book.name}\n\n_Last synced: ${new Date().toISOString()}_\n\n---\n\n${text}`);
results.push({
id: book.id,
name: book.name,
localPath: mdPath,
modifiedTime: book.modifiedTime,
});
} catch (err) {
console.error(`❌ Failed to download ${book.name}:`, err.message);
}
}
// Save cache
await fs.mkdir('data', { recursive: true });
await fs.writeFile(BOOKS_CACHE_PATH, JSON.stringify(results, null, 2));
return results;
}
}
// CLI handler
if (process.argv[1].endsWith('gdrive-client.js')) {
const client = new GDriveClient();
const command = process.argv[2];
switch (command) {
case 'auth':
const code = process.argv[3];
await client.initOAuth();
if (!code) {
// No code provided - show auth URL
await client.authenticate();
} else {
// Code provided - save token
await client.saveToken(code);
}
break;
case 'list':
await client.init();
const books = await client.searchBooks();
console.log('\n📚 HbM: RPG v3 Books:\n');
books.forEach(b => console.log(` - ${b.name}`));
break;
case 'download':
await client.init();
const results = await client.downloadAllBooks();
console.log(`\n✅ Downloaded ${results.length} books to _books/`);
break;
case 'get':
const pattern = process.argv[3];
if (!pattern) {
console.log('Usage: node src/gdrive-client.js get <name-pattern>');
process.exit(1);
}
await client.init();
const book = await client.downloadBook(pattern);
console.log(book.text);
break;
default:
console.log(`
HbM: RPG v3 - Google Drive Client
Commands:
auth <code> - Authenticate with Google (follow OAuth flow)
list - List all HbM books on Google Drive
download - Download all books to _books/
get <pattern> - Get a specific book by name pattern
`);
}
}
export default GDriveClient;