Navigate chrome bookmarks (with folder support) #1462
Replies: 3 comments 1 reply
-
|
Hi there. I wrote this chromemarks Python library a while ago and I think especially the platform-specific Chrome paths would come in handy to improve this script. There are also some models here that could help make this type-safe and offer additional features. HTH :) |
Beta Was this translation helpful? Give feedback.
-
|
I wonder what the icons were supposed to be: The line doesn't appear to render properly... |
Beta Was this translation helpful? Give feedback.
-
|
Here's an improved version that includes Favicons using SQLite access to the Chrome Database, some adjustmens so I could use it with the Vivaldi Browser, strong typings, and other small improvements. Hope you like it! // Name: Bookmarks
// Keyword: bm
import '@johnlindquist/kit'
import { Choice } from '@johnlindquist/kit'
import { join } from 'path'
import sqlite3 from 'sqlite3'
//#region Chromium Bookmark Types
export interface Root {
checksum: string
roots: Roots
}
export interface Roots {
bookmark_bar: Folder
other: Folder
synced: Folder
trash: Folder
}
interface Base {
id: string
date_added: string
date_last_used: string
date_modified?: string
guid: string
meta_info?: MetaInfo
name: string
}
export interface Folder extends Base {
type: 'folder'
children: (Folder | Bookmark)[]
url: undefined
}
export interface Bookmark extends Base {
type: 'url'
url: string
children: undefined
}
export interface MetaInfo {
Description?: string
Thumbnail?: string
power_bookmark_meta?: string
Nickname?: string
ThemeColor?: string
Partner?: string
Bookmarkbar?: string
}
//#endregion
const SUPPORTED_BROWSERS = ['Chrome', 'Vivaldi'] as const
type SupportedBrowser = (typeof SUPPORTED_BROWSERS)[number]
const cacheDefaults = { favicons: null as { [url: string]: string } | null }
const cache = await db(cacheDefaults)
const browserKind = (await env('BOOKMARKS_BROWSER_KIND', {
choices: [...SUPPORTED_BROWSERS],
name: 'Which Browser do you use?',
})) as SupportedBrowser
const { bookmarksJsonFile, faviconsDbFile } = getBrowserInstallationFiles(browserKind)
const favicons = await loadFavicons(faviconsDbFile)
const root = (await readJson(bookmarksJsonFile)) as Root
let bookmarks = root.roots.bookmark_bar.children
// Initializing an array to keep track of the navigation history
let historyStack = []
type OptionValue = 'go-back' | Folder | Bookmark
function buildChoices() {
const createChoice = (item: Folder | Bookmark) => {
if (item.type === 'folder') {
return {
name: item.name,
// Folder icon (can't put it in the code due to a Kit bug)
html: `📁 ${item.name}`,
value: item,
} as Choice<OptionValue>
}
return {
name: item.name,
description: item.url,
keyword: item.meta_info?.Nickname,
img: favicons[item.url],
value: item,
} satisfies Choice<OptionValue>
}
// Generating options based on current level of bookmarks
let choices: Choice<OptionValue>[] = bookmarks.map(createChoice)
// Adding a "go back" option if there is a history in the stack
if (historyStack.length > 0) {
choices = [{ name: '⤴️ ..', description: 'Go back', value: 'go-back' }, ...choices]
}
return choices
}
// Loop to handle user interaction and navigation within bookmarks
while (true) {
const lastSelection = await arg(
{
name: 'Select A Bookmark!',
shortcuts: [
{
name: 'Update Favicons',
visible: true,
bar: 'right' as const,
key: 'ctrl+u',
onPress: async () => {
await loadFavicons(faviconsDbFile, false)
setChoices(buildChoices())
setName('Select A Bookmark!')
},
},
],
},
buildChoices(),
)
if (lastSelection === 'go-back') {
bookmarks = historyStack.pop()
continue
}
const { type, name } = lastSelection
if (type === 'folder') {
// push the old bookmarks into the stack
historyStack.push(bookmarks)
bookmarks = bookmarks.find((bookmark) => bookmark.name === name).children
continue
}
if (type === 'url') {
exec(`open "${lastSelection.url}"`)
break
}
console.log('Unknown type', type)
}
function getBrowserInstallationFiles(browserKind: SupportedBrowser) {
const platform: string = process.platform.toLowerCase()
const installDir = (() => {
switch (true) {
case browserKind === 'Chrome' && platform.includes('linux'):
return home('.config', 'google-chrome', 'Default')
case browserKind === 'Chrome' && platform.includes('darwin'):
return home('Library', 'Application Support', 'Google', 'Chrome', 'Default')
case browserKind === 'Chrome' && platform.includes('win32'):
return home('AppData', 'Local', 'Google', 'Chrome', 'User Data', 'Default')
case browserKind === 'Vivaldi' && platform.includes('win32'):
return home('AppData', 'Local', 'Vivaldi', 'User Data', 'Default')
default:
throw new Error(
`Not implemented: It is unknown where the ${browserKind} bookmarks path for platform ${process.platform} is. Please help us out with a pull request!`,
)
}
})()
if (!pathExistsSync(installDir)) {
throw new Error(
`${browserKind} bookmarks path determined to be at '${installDir}' according to system platform, but nothing exists at that location. Is ${browserKind} installed?`,
)
}
return {
bookmarksJsonFile: join(installDir, 'Bookmarks'),
faviconsDbFile: join(installDir, 'Favicons'),
} as const
}
async function loadFavicons(faviconsDbFile: string, allowCached = true) {
if (allowCached && cache.favicons) {
return cache.favicons
}
setHint('Loading Favicons...');
const db = new sqlite3.Database(faviconsDbFile, sqlite3.OPEN_READONLY, (err) => {
if (err) {
throw err
}
})
type BookmarkFavicon = { page_url: string; image_data: Buffer }
const result = await new Promise<BookmarkFavicon[] | 'locked'>((resolve, reject) => {
db.all<{ page_url: string; image_data: Buffer }>(
'SELECT page_url, image_data FROM icon_mapping INNER JOIN favicons ON favicons.id = icon_mapping.icon_id INNER JOIN main.favicon_bitmaps fb on favicons.id = fb.icon_id',
[],
async (err, rows) => {
if (err) {
if ('code' in err && err.code === 'SQLITE_BUSY') {
resolve('locked')
}
reject(err)
}
resolve(rows)
},
)
})
db.close()
setHint(undefined);
if (result === 'locked') {
return await databaseLockedPrompt(faviconsDbFile)
}
const b64Map = result.reduce((agg, row) => {
const { page_url, image_data } = row
const b64 = `data:image/jpeg;base64,${image_data.toString('base64')}`
agg.set(page_url, b64)
return agg
}, new Map<string, string>())
const b64Data = Object.fromEntries(b64Map.entries())
cache.favicons = b64Data
cache.write().then()
return b64Data
}
async function databaseLockedPrompt(faviconsDbFile: string) {
const choice = await select(
{
hint: 'Cannot read the Favicons database while the browser is still running. Please close it completely to cache the Favicons and continue to try again.',
multiple: false,
},
[
{ name: 'Retry', value: 'retry' },
{ name: 'Continue without Favicons', value: 'without' },
],
)
setHint(null)
switch (choice) {
case 'without':
return new Map<string, string>()
case 'retry':
return await loadFavicons(faviconsDbFile, false)
}
} |
Beta Was this translation helpful? Give feedback.

Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Install script in Script Kit
Beta Was this translation helpful? Give feedback.
All reactions