Assorted shell and Python scripts
1#!/usr/bin/env python
2#
3# DESCRIPTION
4# Search and open Firefox bookmarks from Rofi
5#
6# USAGE
7# - Ensure the imported packages below are available to your Python interpreter.
8# - Must have rofi installed (obviously)
9# - Run script from command line as ./rofifox.py or set to activate with keyboard shortcut
10#
11# TODO
12# - Optimize
13#
14# CHANGELOG
15# 2022-05-05 Jeffrey Serio <hyperreal@fedoraproject.org>
16#
17# Add support for tags.
18#
19# 2022-04-17 Jeffrey Serio <hyperreal@fedoraproject.org>
20#
21# Use parts from https://gist.github.com/iafisher/d624c04940fa46c6d9afb26cb1bf222a
22# Re-use temporary file.
23#
24# 2022-04-13 Jeffrey Serio <hyperreal@fedoraproject.org>
25#
26# Refactor code; no need for query_db() function. Use a dict object for
27# lookups instead of iterating through a list.
28#
29#
30# LICENSE
31# Copyright 2022 Jeffrey Serio <hyperreal@fedoraproject.org>
32#
33# This program is free software: you can redistribute it and/or modify
34# it under the terms of the GNU General Public License as published by
35# the Free Software Foundation, either version 3 of the License, or
36# (at your option) any later version.
37#
38# This program is distributed in the hope that it will be useful,
39# but WITHOUT ANY WARRANTY; without even the implied warranty of
40# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
41# GNU General Public License for more details.
42#
43# You should have received a copy of the GNU General Public License
44# along with this program. If not, see <https://www.gnu.org/licenses/>.
45
46import configparser
47import os
48import shutil
49import sqlite3
50import subprocess
51import tempfile
52import webbrowser as wb
53from collections import namedtuple
54
55
56class Rofifox:
57 Bookmark = namedtuple("Bookmark", ["title", "url", "tags"])
58
59 def __init__(self):
60 self.bookmarks = self.get_bookmarks()
61 self.tags = self.get_tags()
62 self.bm_dict = self.get_bm_dict()
63
64 def get_bookmarks(self) -> list:
65 firefox_path = os.path.join(os.environ["HOME"], ".mozilla/firefox/")
66 conf_path = os.path.join(firefox_path, "profiles.ini")
67 profile = configparser.RawConfigParser()
68 profile.read(conf_path)
69 prof_path = profile.get("Profile0", "Path")
70 sql_path = os.path.join(firefox_path, prof_path, "places.sqlite")
71 tmpdir = tempfile.gettempdir()
72 shutil.copy(sql_path, tmpdir)
73
74 conn = sqlite3.connect(os.path.join(tmpdir, "places.sqlite"))
75 cursor = conn.cursor()
76 cursor.execute(
77 """
78 SELECT
79 moz_places.id,
80 moz_bookmarks.title,
81 moz_places.url
82 FROM
83 moz_bookmarks
84 LEFT JOIN
85 -- The actual URLs are stored in a separate moz_places table, which is pointed
86 -- at by the moz_bookmarks.fk field.
87 moz_places
88 ON
89 moz_bookmarks.fk = moz_places.id
90 WHERE
91 -- Type 1 is for bookmarks; type 2 is for folders and tags.
92 moz_bookmarks.type = 1
93 AND
94 moz_bookmarks.title IS NOT NULL
95 ;
96 """
97 )
98
99 rows = cursor.fetchall()
100
101 bookmark_list = list()
102 for place_id, title, url in rows:
103 # A tag relationship is established by row in the moz_bookmarks table with NULL
104 # title where parent is the tag ID (in moz_bookmarks) and fk is the URL.
105 cursor.execute(
106 """
107 SELECT
108 A.title
109 FROM
110 moz_bookmarks A, moz_bookmarks B
111 WHERE
112 A.id <> B.id
113 AND
114 B.parent = A.id
115 AND
116 B.title IS NULL
117 AND
118 B.fk = ?;
119 """,
120 (place_id,),
121 )
122 tag_names = [r[0] for r in cursor.fetchall()]
123 bookmark_list.append(self.Bookmark(title, url, tag_names))
124
125 conn.close()
126 return bookmark_list
127
128 def get_tags(self) -> set:
129 tags = set()
130 for item in self.bookmarks:
131 if item.tags:
132 for tag in item.tags:
133 tags.add(tag)
134 return tags
135
136 def get_bm_dict(self) -> dict:
137 bm_dict = dict()
138 for item in self.bookmarks:
139 bm_dict.setdefault(item.title, []).append(item.url)
140 return bm_dict
141
142 def bookmarks_to_str(self) -> str:
143 bookmarks = [item.title for item in self.bookmarks]
144 return "\n".join(bookmarks)
145
146 def tags_to_str(self) -> str:
147 tags = [tag for tag in sorted(self.tags)]
148 return "\n".join(tags)
149
150 def tag_items_to_str(self, tag: str) -> str:
151 tag_items = list()
152 for item in self.bookmarks:
153 if tag in item.tags:
154 tag_items.append(item.title)
155 return "\n".join(tag_items)
156
157 def open_url(self, item: str):
158 url = self.bm_dict.get(item)[0]
159 if url:
160 wb.open(url, new=2)
161
162 def run_cmd(self, input: str, option: str) -> subprocess.CompletedProcess:
163 return subprocess.run(
164 ["rofi", "-dmenu", "-i", "-p", "%s" % option],
165 input=input,
166 capture_output=True,
167 text=True,
168 )
169
170
171def run_rofifox():
172 rofifox = Rofifox()
173
174 rofi = rofifox.run_cmd("%s\n%s" % ("Bookmarks", "Tags"), "Rofifox")
175 if rofi.stdout.strip() == "Bookmarks":
176 bookmarks = rofifox.bookmarks_to_str()
177 bm = rofifox.run_cmd(bookmarks, "Bookmarks")
178 if bm.stdout.strip():
179 rofifox.open_url(bm.stdout.strip())
180 elif rofi.stdout.strip() == "Tags":
181 tags = rofifox.tags_to_str()
182 tag = rofifox.run_cmd(tags, "Tags")
183 tag_items = rofifox.tag_items_to_str(tag.stdout.strip())
184 tag_item = rofifox.run_cmd(tag_items, tag.stdout.strip())
185 if tag_item.stdout.strip():
186 rofifox.open_url(tag_item.stdout.strip())
187
188
189run_rofifox()