Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,35 @@ program
"Done file location (tracks which URLs have been downloaded before)",
"done.json"
)
.option(
"-s, --sort-order <order>",
"Sort order: 'newest' or 'oldest'",
"newest"
)
.requiredOption("-e, --email <email>", "Perplexity email")
.parse();

const options = program.opts();

async function main(): Promise<void> {
// Normalize and validate sort order
let sortOrder = options.sortOrder;
if (sortOrder === "o" || sortOrder === "old") {
sortOrder = "oldest";
} else if (sortOrder === "n" || sortOrder === "new") {
sortOrder = "newest";
}

if (sortOrder !== "newest" && sortOrder !== "oldest") {
console.error("Error: Sort order must be 'newest' or 'oldest'");
process.exit(1);
}

await exportLibrary({
outputDir: options.output,
doneFilePath: options.doneFile,
email: options.email,
sortOrder: sortOrder,
});
}

Expand Down
3 changes: 2 additions & 1 deletion src/exportLibrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface ExportLibraryOptions {
outputDir: string;
doneFilePath: string;
email: string;
sortOrder?: 'newest' | 'oldest';
}

export default async function exportLibrary(options: ExportLibraryOptions) {
Expand All @@ -35,7 +36,7 @@ export default async function exportLibrary(options: ExportLibraryOptions) {
const page = await browser.newPage();

await login(page, options.email);
const conversations = await getConversations(page, doneFile);
const conversations = await getConversations(page, doneFile, options.sortOrder || 'newest');

console.log(`Found ${conversations.length} new conversations to process`);

Expand Down
101 changes: 92 additions & 9 deletions src/listConversations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,83 @@ import { Page } from "puppeteer";
import { Conversation, DoneFile } from "./types";
import { sleep } from "./utils";

async function setSortOrder(
page: Page,
sortOrder: "newest" | "oldest",
): Promise<void> {
console.log(`Setting sort order to: ${sortOrder}`);

try {
// Find the sort button by looking for text containing either "Sort: Newest" or "Sort: Oldest"
await page.waitForFunction(
() => {
const buttons = Array.from(document.querySelectorAll("button"));
return buttons.some((button) => {
const text = button.textContent || "";
return text.includes("Sort: Newest") || text.includes("Sort: Oldest");
});
},
{ timeout: 10000 },
);

// Click the sort button
await page.evaluate(() => {
const buttons = Array.from(document.querySelectorAll("button"));
const sortButton = buttons.find((button) => {
const text = button.textContent || "";
return text.includes("Sort: Newest") || text.includes("Sort: Oldest");
});
if (sortButton) {
(sortButton as HTMLElement).click();
} else {
throw new Error("Could not find sort button");
}
});

// Wait for dropdown menu to appear
await sleep(1000);

// Find and click the appropriate sort option
const targetText = sortOrder === "newest" ? "Newest First" : "Oldest First";

// Wait for the dropdown menu and find the option by text
await page.waitForFunction(
(text) => {
const elements = Array.from(
document.querySelectorAll('[role="menuitem"]'),
);
return elements.some((el) => el.textContent?.includes(text));
},
{ timeout: 5000 },
targetText,
);

// Click the option containing the target text
await page.evaluate((text) => {
const elements = Array.from(
document.querySelectorAll('[role="menuitem"]'),
);
const target = elements.find((el) => el.textContent?.includes(text));
if (target) {
(target as HTMLElement).click();
} else {
throw new Error(`Could not find menu item with text: ${text}`);
}
}, targetText);


// Wait for UI to update
await sleep(2000);

} catch (error) {
console.error(`Failed to set sort order: ${error}`);
throw error;
}
}

export async function scrollToBottomOfConversations(
page: Page,
doneFile: DoneFile
doneFile: DoneFile,
): Promise<void> {
// Scroll to bottom and wait for more items until no new items load
let previousHeight = 0;
Expand All @@ -17,7 +91,7 @@ export async function scrollToBottomOfConversations(
// Check if we've hit any processed URLs
const foundProcessed = await page.evaluate((processedUrls) => {
const items = Array.from(
document.querySelectorAll('div[data-testid="thread-title"]')
document.querySelectorAll('div[data-testid="thread-title"]'),
).map((div: Element) => div.closest("a") as HTMLAnchorElement);
return items.some((item) => processedUrls.includes(item.href));
}, doneFile.processedUrls);
Expand Down Expand Up @@ -48,27 +122,36 @@ export async function scrollToBottomOfConversations(

export async function getConversations(
page: Page,
doneFile: DoneFile
doneFile: DoneFile,
sortOrder: "newest" | "oldest" = "newest",
): Promise<Conversation[]> {
console.log("Navigating to library...");
await page.goto("https://www.perplexity.ai/library");

await page.waitForSelector('div[data-testid="thread-title"]');
await page.waitForSelector('div[data-testid^="thread-title-"]');

// Set the sort order before scrolling
await setSortOrder(page, sortOrder);

await scrollToBottomOfConversations(page, doneFile);

// Get all conversation links
const conversations = await page.evaluate(() => {
const items = Array.from(
document.querySelectorAll('div[data-testid="thread-title"]')
document.querySelectorAll('div[data-testid^="thread-title-"]'),
).map((div: Element) => div.closest("a") as HTMLAnchorElement);
return items.map((item) => ({
title: item.textContent?.trim() || "Untitled",
url: item.href,
}));
});

// Filter out already processed conversations and reverse the order
return conversations
.filter((conv) => !doneFile.processedUrls.includes(conv.url))
.reverse();
// Filter out already processed conversations
const filtered = conversations.filter(
(conv) => !doneFile.processedUrls.includes(conv.url),
);

// For "newest" sort order, reverse to process oldest first (as original behavior)
// For "oldest" sort order, keep the order as-is since we want to process oldest first
return sortOrder === "newest" ? filtered.reverse() : filtered;
}
2 changes: 0 additions & 2 deletions src/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ export async function login(page: Page, email: string): Promise<void> {
console.log("Navigating to Perplexity...");
await page.goto("https://www.perplexity.ai/");

await page.click("button::-p-text('Accept All Cookies')");

// Wait for email input and enter credentials
await page.waitForSelector('input[type="email"]');
await page.type('input[type="email"]', email);
Expand Down
13 changes: 11 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,24 @@ export async function loadDoneFile(doneFilePath: string): Promise<DoneFile> {
try {
const content = await fs.readFile(doneFilePath, "utf-8");
return JSON.parse(content);
} catch (error) {
} catch (error: any) {
if (error.code === "ENOENT") {
const defaultDoneFile = { processedUrls: [] };
console.log(`Created empty ${doneFilePath}`);
await fs.writeFile(
doneFilePath,
JSON.stringify(defaultDoneFile, null, 2),
);
return defaultDoneFile;
}
console.error(`Error loading done file ${doneFilePath}:`, error);
return { processedUrls: [] };
}
}

export async function saveDoneFile(
doneFile: DoneFile,
doneFilePath: string
doneFilePath: string,
): Promise<void> {
await fs.writeFile(doneFilePath, JSON.stringify(doneFile, null, 2));
}
Expand Down