Flow.Launcher/Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs

232 lines
7.8 KiB
C#
Raw Normal View History

using System.Collections.Generic;
2021-09-21 18:50:51 +00:00
using System.IO;
using System.Text.Json;
2025-03-19 20:04:19 +00:00
using System;
using Flow.Launcher.Plugin.BrowserBookmark.Models;
2025-03-20 08:03:08 +00:00
using Microsoft.Data.Sqlite;
2021-09-21 18:50:51 +00:00
namespace Flow.Launcher.Plugin.BrowserBookmark;
public abstract class ChromiumBookmarkLoader : IBookmarkLoader
2021-09-21 18:50:51 +00:00
{
2025-04-02 14:39:30 +00:00
private static readonly string ClassName = nameof(ChromiumBookmarkLoader);
2025-03-19 20:04:19 +00:00
private readonly string _faviconCacheDir;
protected ChromiumBookmarkLoader()
{
2025-04-02 14:39:30 +00:00
_faviconCacheDir = Main._faviconCacheDir;
2025-03-19 20:04:19 +00:00
}
public abstract List<Bookmark> GetBookmarks();
protected List<Bookmark> LoadBookmarks(string browserDataPath, string name)
2021-09-21 18:50:51 +00:00
{
var bookmarks = new List<Bookmark>();
if (!Directory.Exists(browserDataPath)) return bookmarks;
var paths = Directory.GetDirectories(browserDataPath);
2023-12-18 06:30:18 +00:00
foreach (var profile in paths)
2021-09-21 18:50:51 +00:00
{
var bookmarkPath = Path.Combine(profile, "Bookmarks");
if (!File.Exists(bookmarkPath))
continue;
2021-09-21 18:50:51 +00:00
2025-03-19 20:04:19 +00:00
// Register bookmark file monitoring (direct call to Main.RegisterBookmarkFile)
try
{
if (File.Exists(bookmarkPath))
{
2025-04-09 05:12:05 +00:00
Main.RegisterBookmarkFile(bookmarkPath);
2025-03-19 20:04:19 +00:00
}
}
catch (Exception ex)
{
Main._context.API.LogException(ClassName, $"Failed to register bookmark file monitoring: {bookmarkPath}", ex);
2025-03-19 20:04:19 +00:00
}
2023-12-18 06:30:18 +00:00
var source = name + (Path.GetFileName(profile) == "Default" ? "" : $" ({Path.GetFileName(profile)})");
2025-03-19 20:04:19 +00:00
var profileBookmarks = LoadBookmarksFromFile(bookmarkPath, source);
// Load favicons after loading bookmarks
var faviconDbPath = Path.Combine(profile, "Favicons");
if (File.Exists(faviconDbPath))
{
LoadFaviconsFromDb(faviconDbPath, profileBookmarks);
}
bookmarks.AddRange(profileBookmarks);
2021-09-21 18:50:51 +00:00
}
return bookmarks;
}
protected static List<Bookmark> LoadBookmarksFromFile(string path, string source)
{
var bookmarks = new List<Bookmark>();
2022-01-08 22:58:12 +00:00
if (!File.Exists(path))
return bookmarks;
using var jsonDocument = JsonDocument.Parse(File.ReadAllText(path));
if (!jsonDocument.RootElement.TryGetProperty("roots", out var rootElement))
return bookmarks;
EnumerateRoot(rootElement, bookmarks, source);
return bookmarks;
}
private static void EnumerateRoot(JsonElement rootElement, ICollection<Bookmark> bookmarks, string source)
{
foreach (var folder in rootElement.EnumerateObject())
{
if (folder.Value.ValueKind != JsonValueKind.Object)
continue;
2025-03-19 20:04:19 +00:00
// Fix for Opera. It stores bookmarks slightly different than chrome.
if (folder.Name == "custom_root")
EnumerateRoot(folder.Value, bookmarks, source);
else
EnumerateFolderBookmark(folder.Value, bookmarks, source);
2021-09-21 18:50:51 +00:00
}
}
2021-09-21 18:50:51 +00:00
private static void EnumerateFolderBookmark(JsonElement folderElement, ICollection<Bookmark> bookmarks,
string source)
{
if (!folderElement.TryGetProperty("children", out var childrenElement))
return;
foreach (var subElement in childrenElement.EnumerateArray())
2021-09-21 18:50:51 +00:00
{
if (subElement.TryGetProperty("type", out var type))
2021-09-21 18:50:51 +00:00
{
switch (type.GetString())
2023-12-18 06:30:18 +00:00
{
case "folder":
case "workspace": // Edge Workspace
EnumerateFolderBookmark(subElement, bookmarks, source);
break;
default:
bookmarks.Add(new Bookmark(
subElement.GetProperty("name").GetString(),
subElement.GetProperty("url").GetString(),
source));
break;
2021-09-21 18:50:51 +00:00
}
}
else
{
2025-04-09 04:14:07 +00:00
Main._context.API.LogError(ClassName, $"type property not found for {subElement.GetString()}");
}
2021-09-21 18:50:51 +00:00
}
}
2025-03-19 20:04:19 +00:00
private void LoadFaviconsFromDb(string dbPath, List<Bookmark> bookmarks)
{
2025-04-09 09:29:28 +00:00
// Use a copy to avoid lock issues with the original file
var tempDbPath = Path.Combine(_faviconCacheDir, $"tempfavicons_{Guid.NewGuid()}.db");
2025-03-19 20:04:19 +00:00
try
{
2025-04-09 09:29:28 +00:00
File.Copy(dbPath, tempDbPath, true);
}
catch (Exception ex)
{
try
{
if (File.Exists(tempDbPath))
{
File.Delete(tempDbPath);
}
}
catch (Exception ex1)
{
Main._context.API.LogException(ClassName, $"Failed to delete temporary favicon DB: {tempDbPath}", ex1);
}
2025-04-09 09:29:28 +00:00
Main._context.API.LogException(ClassName, $"Failed to copy favicon DB: {dbPath}", ex);
return;
}
2025-03-19 20:04:19 +00:00
2025-04-09 09:29:28 +00:00
try
{
using var connection = new SqliteConnection($"Data Source={tempDbPath}");
connection.Open();
2025-03-19 20:04:19 +00:00
2025-04-09 09:29:28 +00:00
foreach (var bookmark in bookmarks)
2025-03-19 20:04:19 +00:00
{
2025-04-09 09:29:28 +00:00
try
2025-03-19 20:04:19 +00:00
{
2025-04-09 09:29:28 +00:00
var url = bookmark.Url;
if (string.IsNullOrEmpty(url)) continue;
2025-03-19 20:04:19 +00:00
2025-04-09 09:29:28 +00:00
// Extract domain from URL
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
continue;
2025-03-19 20:04:19 +00:00
2025-04-09 09:29:28 +00:00
var domain = uri.Host;
2025-03-19 20:04:19 +00:00
2025-04-09 09:29:28 +00:00
using var cmd = connection.CreateCommand();
cmd.CommandText = @"
SELECT f.id, b.image_data
FROM favicons f
JOIN favicon_bitmaps b ON f.id = b.icon_id
JOIN icon_mapping m ON f.id = m.icon_id
WHERE m.page_url LIKE @url
ORDER BY b.width DESC
LIMIT 1";
2025-03-19 20:04:19 +00:00
2025-04-09 09:29:28 +00:00
cmd.Parameters.AddWithValue("@url", $"%{domain}%");
2025-03-19 20:04:19 +00:00
2025-04-09 09:29:28 +00:00
using var reader = cmd.ExecuteReader();
if (!reader.Read() || reader.IsDBNull(1))
continue;
2025-03-20 08:43:38 +00:00
2025-04-09 09:29:28 +00:00
var iconId = reader.GetInt64(0).ToString();
var imageData = (byte[])reader["image_data"];
2025-03-20 08:43:38 +00:00
2025-04-09 09:29:28 +00:00
if (imageData is not { Length: > 0 })
continue;
2025-03-20 08:43:38 +00:00
2025-04-09 09:29:28 +00:00
var faviconPath = Path.Combine(_faviconCacheDir, $"chromium_{domain}_{iconId}.png");
SaveBitmapData(imageData, faviconPath);
2025-04-09 09:29:28 +00:00
bookmark.FaviconPath = faviconPath;
}
catch (Exception ex)
{
Main._context.API.LogException(ClassName, $"Failed to extract bookmark favicon: {bookmark.Url}", ex);
2025-03-19 20:04:19 +00:00
}
}
2025-04-09 09:29:28 +00:00
// https://github.com/dotnet/efcore/issues/26580
SqliteConnection.ClearPool(connection);
connection.Close();
}
catch (Exception ex)
{
Main._context.API.LogException(ClassName, $"Failed to connect to SQLite: {tempDbPath}", ex);
}
// Delete temporary file
try
{
File.Delete(tempDbPath);
2025-03-19 20:04:19 +00:00
}
catch (Exception ex)
{
2025-04-09 09:29:28 +00:00
Main._context.API.LogException(ClassName, $"Failed to delete temporary favicon DB: {tempDbPath}", ex);
2025-03-19 20:04:19 +00:00
}
}
private static void SaveBitmapData(byte[] imageData, string outputPath)
2025-03-19 20:04:19 +00:00
{
try
{
2025-03-20 08:03:08 +00:00
File.WriteAllBytes(outputPath, imageData);
2025-03-19 20:04:19 +00:00
}
catch (Exception ex)
{
Main._context.API.LogException(ClassName, $"Failed to save image: {outputPath}", ex);
2025-03-19 20:04:19 +00:00
}
}
}