tabman 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tabman-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Anshul Gada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
tabman-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.1
2
+ Name: tabman
3
+ Version: 0.1.0
4
+ Home-page: https://github.com/Anshulgada/brave-tab-manager
5
+ Author: Anshul Gada
6
+ License-File: LICENSE
7
+ Requires-Dist: playwright
8
+ Requires-Dist: requests
9
+ Requires-Dist: beautifulsoup4
10
+ Requires-Dist: google-generativeai
11
+ Requires-Dist: google-api-python-client
12
+ Requires-Dist: tenacity
13
+ Requires-Dist: openai
14
+ Requires-Dist: python-dotenv
15
+ Requires-Dist: ollama
16
+ Requires-Dist: markdown
tabman-0.1.0/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # Tabman: Brave Tab Manager
2
+
3
+ A command-line tool designed to streamline your browsing experience by intelligently organizing your open tabs from the Brave browser. `tabman` helps you categorize, tag, and save your browsing sessions, making it easier to find and revisit websites later.
4
+
5
+ ## Features
6
+
7
+ - **Tab Retrieval:** Fetches all open tabs from Brave using its remote debugging interface.
8
+ - **Intelligent Categorization:** Uses AI-powered language models to categorize your tabs:
9
+ - **Google Gemini:** Employs the Gemini 2.0 Flash model for advanced and fast categorization.
10
+ - **Mistral AI:** Leverages Mistral's large language models as an alternative to Gemini.
11
+ - **Ollama:** Enables local categorization using Ollama, supporting a variety of LLMs (e.g., Llama 2, Mistral).
12
+ - **Dynamic Tagging:** Generates a list of concise and relevant keywords to describe the content of each tab.
13
+ - **Flexible Saving:**
14
+ - Saves the tab data (title, URL, main category, and tags) as a JSON file, organized in date-based subfolders within the `data/` directory.
15
+ - Creates a Markdown version of each tab session, also stored in the date-based subfolders within the `data/` directory.
16
+ - Maintains a central `all_tabs.md` file in the `data/` directory, where all tab data is appended for easy access.
17
+ - **Secure API Key Management:** Utilizes a `.env` file to securely store API keys, which can be set using command-line arguments.
18
+
19
+ ## Installation
20
+
21
+ 1. **Clone the repository:**
22
+
23
+ ```bash
24
+ git clone https://github.com/Anshulgada/brave-tab-manager.git
25
+ ```
26
+
27
+
28
+ 2. **Navigate to the project directory:**
29
+
30
+ ```bash
31
+ cd Brave Tab Manager
32
+ ```
33
+
34
+ This assumes that your project folder is named `Brave Tab Manager`.
35
+
36
+ 3. **Install `pipx` (if you don't have it):**
37
+
38
+ ```bash
39
+ python -m pip install --user pipx
40
+ python -m pipx ensurepath
41
+ ```
42
+
43
+ This ensures that `pipx` is installed globally and its executables are present in your system's PATH. You may need to restart your terminal for these changes to take effect.
44
+
45
+ 4. **Install `tabman` using `pipx`:**
46
+ ```bash
47
+ pipx install -e .
48
+ ```
49
+ This command installs your tool into an isolated environment and makes the `tabman` command accessible from anywhere. Make sure you run this command from the root of the project directory, i.e. `Brave Tab Manager`.
50
+
51
+ ## Usage
52
+
53
+ 1. **Enable Remote Debugging in Brave:**
54
+ To allow `tabman` to connect with your Brave browser, you must enable remote debugging. Here's how:
55
+
56
+ - **Windows:**
57
+ 1. Right-click on the Brave shortcut in your Start Menu or Taskbar and select "Properties."
58
+ 2. In the "Target" field, add `--remote-debugging-port=9222` (or your preferred port number) to the end of the existing path, ensuring there's a space between the path and the flag. For example:
59
+ ```
60
+ "C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe" --remote-debugging-port=9222
61
+ ```
62
+ 3. Click "Apply" and "OK."
63
+ - **macOS/Linux:** Modify the Brave shortcut (or application launcher) to include the `--remote-debugging-port=9222` flag in the command line that launches Brave. Consult your system's documentation for the specific method.
64
+ - After enabling remote debugging, make sure that you have restarted your Brave browser.
65
+
66
+ 2. **Run the `tabman` tool:**
67
+
68
+ ```bash
69
+ tabman [OPTIONS]
70
+ ```
71
+
72
+ You can use the following options:
73
+
74
+ - `-c`, `--categorize`: Categorize your open tabs using the selected LLM model, and save the data. This is not necessary when setting the api keys.
75
+ - `-v`, `--version`: Display the version of `tabman` and exits.
76
+ - `-m`, `--model <MODEL>`: Specify the LLM model for categorization:
77
+ - `gemini` (default): Uses Google Gemini.
78
+ - `mistral`: Uses Mistral AI.
79
+ - `ollama`: Uses a local LLM with Ollama.
80
+ - `--save-keys`: Save the provided API keys to the `.env` file. Use this in combination with `-mk` and `-gk` options to save the keys.
81
+ - `-mk`, `--mistral-key <KEY>`: Provide your Mistral AI API key.
82
+ - `-gk`, `--gemini-key <KEY>`: Provide your Google Gemini API key.
83
+ - `-om`, `--ollama-model <MODEL>`: Provide the Ollama model name (e.g., `llama2`, `mistral`). Default is `llama2`.
84
+
85
+ **Examples:**
86
+
87
+ - **Categorize tabs using the default Gemini model:**
88
+ ```bash
89
+ tabman -c
90
+ ```
91
+ - **Categorize tabs using Mistral:**
92
+ ```bash
93
+ tabman -c -m mistral
94
+ ```
95
+ - **Categorize tabs using a specific Ollama model:**
96
+ ```bash
97
+ tabman -c -m ollama -om llama3.2
98
+ ```
99
+ - **Save API keys to the `.env` file:**
100
+
101
+ ```bash
102
+ tabman --save-keys -gk YOUR_GEMINI_API_KEY -mk YOUR_MISTRAL_API_KEY
103
+ ```
104
+ Replace `YOUR_GEMINI_API_KEY` and `YOUR_MISTRAL_API_KEY` with your actual API keys.
105
+
106
+ - **Show help message:**
107
+ ```bash
108
+ tabman
109
+ ```
110
+
111
+ ## Configuration
112
+
113
+ - **API Keys:** API keys for Gemini and Mistral can be provided either directly via command-line arguments or saved to a `.env` file for later use. The tool will read keys from environment variables, and then the command line arguments and the `.env` file when using the tool.
114
+ - **Ollama:** If you select Ollama model, then you will have to install and run ollama on your system with your desired models.
115
+
116
+ ## Development and Testing
117
+
118
+ - To avoid re-installing the package again and again, during local development, you should run the tool inside your virtual environment. For this you have to activate your environment and then run `pip install -e .` from the root directory. Now you can use the tool with the activated virtual environment. Whenever you want to update the global version then you have to uninstall the current version using `pipx uninstall tabman` and install again using `pipx install .`.
119
+
120
+ - **Running tests:**
121
+ - The tests are located in the `tabman/tests` directory. You can run the tests using `pytest` rom the root of your project i.e. where `setup.py` exists.
122
+ ```bash
123
+ pytest -v -s tabman/tests
124
+ ```
125
+
126
+ ## Dependencies
127
+
128
+ - `openai`
129
+ - `ollama`
130
+ - `markdown`
131
+ - `requests`
132
+ - `tenacity`
133
+ - `playwright`
134
+ - `python-dotenv`
135
+ - `beautifulsoup4`
136
+ - `google-generativeai`
137
+ - `google-api-python-client`
138
+
139
+ ## License
140
+
141
+ This project is licensed under the MIT License.
tabman-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
tabman-0.1.0/setup.py ADDED
@@ -0,0 +1,26 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="tabman",
5
+ author="Anshul Gada",
6
+ version="0.1.0",
7
+ url="https://github.com/Anshulgada/brave-tab-manager",
8
+ packages=find_packages(),
9
+ install_requires=[
10
+ "playwright",
11
+ "requests",
12
+ "beautifulsoup4",
13
+ "google-generativeai",
14
+ "google-api-python-client",
15
+ "tenacity",
16
+ "openai",
17
+ "python-dotenv",
18
+ "ollama",
19
+ "markdown",
20
+ ],
21
+ entry_points={
22
+ "console_scripts": [
23
+ "tabman = tabman.main:entry_point",
24
+ ],
25
+ },
26
+ )
File without changes
@@ -0,0 +1,259 @@
1
+ import re
2
+ import os
3
+ import openai
4
+ import ollama
5
+ import asyncio
6
+ import logging
7
+ import google.generativeai as genai
8
+ from dotenv import load_dotenv
9
+ from asyncio import TimeoutError
10
+ from urllib.parse import urlparse
11
+ from typing import List, Dict, Optional
12
+ from .tab_capture import get_brave_tabs
13
+ from .content_fetcher import get_content_from_url
14
+ from .tab_saver import save_tabs_to_json, convert_json_to_markdown
15
+ from tenacity import retry, stop_after_attempt, wait_fixed, retry_if_exception_type
16
+
17
+
18
+ # Silence the google-generativeai logs, has to be done before the config import
19
+ logging.getLogger("google.generativeai").setLevel(logging.ERROR)
20
+ load_dotenv() # Load the environment variables
21
+
22
+ # GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
23
+ genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
24
+ gemini_model = genai.GenerativeModel("gemini-2.0-flash-exp")
25
+
26
+
27
+ MAIN_CATEGORIES = {
28
+ "YouTube": ["youtube.com", "youtu.be"],
29
+ "Coding": ["github.com", "stackoverflow.com"],
30
+ "Articles": [],
31
+ "Social Media": ["facebook.com", "twitter.com", "instagram.com"],
32
+ "News": [],
33
+ "Personal": [],
34
+ "Other": [],
35
+ }
36
+
37
+
38
+ def get_main_category(url: str) -> str:
39
+ """
40
+ Determines the main category of a URL based on predefined rules.
41
+
42
+ Args:
43
+ url (str): The URL to analyze.
44
+
45
+ Returns:
46
+ str: The main category.
47
+ """
48
+ parsed_url = urlparse(url)
49
+ netloc = parsed_url.netloc
50
+
51
+ for category, domains in MAIN_CATEGORIES.items():
52
+ for domain in domains:
53
+ if domain in netloc:
54
+ return category
55
+ return "Other"
56
+
57
+
58
+ def capitalize_tags(tags: List[str]) -> List[str]:
59
+ """Capitalizes the first letter of each word in the tag.
60
+
61
+ Args:
62
+ tags(list): list of tags
63
+
64
+ Returns:
65
+ list: capitalized tags
66
+ """
67
+ return [" ".join(word.capitalize() for word in tag.split()) for tag in tags]
68
+
69
+
70
+ def post_process_tags(tags: List[str]) -> List[str]:
71
+ """
72
+ Post processes tags by removing empty string, duplicate tags, and extra text.
73
+
74
+ Args:
75
+ tags(list): List of tags
76
+
77
+ Returns:
78
+ list(str): A filtered list of tags.
79
+ """
80
+ filtered_tags = [tag for tag in tags if tag] # Remove empty strings
81
+ filtered_tags = list(set(filtered_tags)) # Remove duplicates
82
+ filtered_tags = [
83
+ re.sub(
84
+ r"^(tag|tags|keyword|keywords|topic|topics|sub):?\s*",
85
+ "",
86
+ tag,
87
+ flags=re.IGNORECASE,
88
+ ).strip()
89
+ for tag in filtered_tags
90
+ ] # remove extra text
91
+ return capitalize_tags(filtered_tags)
92
+
93
+
94
+ @retry(
95
+ stop=stop_after_attempt(3),
96
+ wait=wait_fixed(2),
97
+ retry=retry_if_exception_type(TimeoutError),
98
+ )
99
+ async def generate_tags(
100
+ content: Optional[str],
101
+ main_category: str,
102
+ model_type: str = "gemini",
103
+ ollama_model: str = "llama3.2",
104
+ ) -> Optional[List[str]]:
105
+ """
106
+ Generates a list of tags for the given content, using the Gemini, Mistral, or Ollama API.
107
+
108
+ Args:
109
+ content (str): The content to be tagged.
110
+ main_category (str): The main category of the content.
111
+ model_type(str): The model used to generate the tags (gemini or mistral or ollama)
112
+ ollama_model(str): The ollama model to use for generating tags
113
+
114
+ Returns:
115
+ list: A list of tags (or None if tag generation fails).
116
+ """
117
+ if not content:
118
+ return None
119
+ try:
120
+ prompt = f"""
121
+ Given the following text content:
122
+ {content}
123
+ And given that the main category is: {main_category}.
124
+ Generate a comma separated list of the most relevant and concise keywords to describe the content.
125
+ Do not use full sentences or phrases as tags.
126
+ Maximum 5 tags.
127
+ Only return the comma separated list of keywords. Do not add any extra text.
128
+ """
129
+ if model_type == "gemini":
130
+
131
+ async def generate_with_timeout():
132
+ return await asyncio.to_thread(gemini_model.generate_content, prompt)
133
+
134
+ response = await asyncio.wait_for(
135
+ generate_with_timeout(), timeout=10
136
+ ) # Run in thread to not block main thread and apply timeout
137
+ elif model_type == "mistral":
138
+ client = openai.OpenAI(api_key=os.getenv("MISTRAL_API_KEY"))
139
+
140
+ async def generate_with_timeout():
141
+ return await asyncio.to_thread(
142
+ client.chat.completions.create,
143
+ model="mistral-tiny",
144
+ messages=[{"role": "user", "content": prompt}],
145
+ )
146
+
147
+ response = await asyncio.wait_for(generate_with_timeout(), timeout=10)
148
+ if response and response.choices:
149
+ return post_process_tags(
150
+ [
151
+ tag.strip()
152
+ for tag in response.choices[0].message.content.split(",")
153
+ ]
154
+ )
155
+ else:
156
+ return ["Other"]
157
+ elif model_type == "ollama":
158
+
159
+ async def generate_with_timeout():
160
+ return await asyncio.to_thread(
161
+ ollama.generate, model=ollama_model, prompt=prompt
162
+ )
163
+
164
+ response = await asyncio.wait_for(generate_with_timeout(), timeout=10)
165
+ if response and response["response"]:
166
+ return post_process_tags(
167
+ [tag.strip() for tag in response["response"].split(",")]
168
+ )
169
+ else:
170
+ return ["Other"]
171
+
172
+ else:
173
+ return ["Other"]
174
+ if response and response.text:
175
+ return post_process_tags([tag.strip() for tag in response.text.split(",")])
176
+ else:
177
+ return ["Other"] # Return other if no proper response
178
+ except TimeoutError as e:
179
+ print(f"Error during tag generation, request timed out: {e}")
180
+ return None
181
+ except Exception as e:
182
+ print(f"Error during tag generation: {e}")
183
+ return None
184
+
185
+
186
+ async def categorize_tabs(
187
+ tabs: List[Dict[str, str]],
188
+ model_type: str = "gemini",
189
+ ollama_model: str = "llama3.2",
190
+ ) -> List[Dict[str, str]]:
191
+ """
192
+ Categorizes a list of tabs based on their content.
193
+
194
+ Args:
195
+ tabs (list): A list of dictionaries, each with 'title' and 'url' keys.
196
+ model_type(str): The model used to generate the tags (gemini or mistral or ollama)
197
+ ollama_model(str): The ollama model to use for generating tags
198
+
199
+ Returns:
200
+ list: A list of dictionaries, each with 'title', 'url', 'main_category', and 'tags' keys.
201
+ """
202
+ categorized_tabs = []
203
+ for tab in tabs:
204
+ print(f"Categorizing tab: {tab['title']}")
205
+ url = tab["url"]
206
+ main_category = get_main_category(url)
207
+ content = await asyncio.to_thread(get_content_from_url, url)
208
+ if content:
209
+ tags = await generate_tags(content, main_category, model_type, ollama_model)
210
+ tab["main_category"] = main_category
211
+ tab["tags"] = tags if tags else ["Other"]
212
+ else:
213
+ tab["main_category"] = "Other"
214
+ tab["tags"] = ["Other"]
215
+
216
+ categorized_tabs.append(tab)
217
+ return categorized_tabs
218
+
219
+
220
+ async def main_categorizer(
221
+ model_type: str = "gemini",
222
+ save_keys: bool = False,
223
+ mistral_key=None,
224
+ gemini_key=None,
225
+ ollama_model="llama3.2",
226
+ ):
227
+ """
228
+ Main function for categorizing and saving tabs
229
+ Args:
230
+ model_type(str): The model used to generate the tags (gemini or mistral or ollama)
231
+ save_keys (bool): If we want to save the keys
232
+ mistral_key (str): Mistral api key if we want to set using command line args
233
+ gemini_key (str): Gemini api key if we want to set using command line args
234
+ ollama_model (str): The ollama model to use for generating tags
235
+ """
236
+ if save_keys:
237
+ if gemini_key:
238
+ os.environ["GEMINI_API_KEY"] = gemini_key
239
+ if mistral_key:
240
+ os.environ["MISTRAL_API_KEY"] = mistral_key
241
+
242
+ tabs = await get_brave_tabs()
243
+ if tabs:
244
+ categorized_tabs = await categorize_tabs(tabs, model_type, ollama_model)
245
+ json_file = save_tabs_to_json(categorized_tabs)
246
+ print(f"Saved tabs to JSON: {json_file}")
247
+
248
+ markdown_file = convert_json_to_markdown(json_file)
249
+ print(f"Converted JSON to markdown: {markdown_file}")
250
+ else:
251
+ print("Could not get the tabs from brave browser.")
252
+
253
+
254
+ if __name__ == "__main__":
255
+
256
+ async def main():
257
+ await main_categorizer()
258
+
259
+ asyncio.run(main())
@@ -0,0 +1,156 @@
1
+ import os
2
+ import requests
3
+ from typing import Optional
4
+ from bs4 import BeautifulSoup
5
+ from googleapiclient.discovery import build
6
+ from googleapiclient.errors import HttpError
7
+
8
+ # from config import GEMINI_API_KEY
9
+ from dotenv import load_dotenv
10
+
11
+ load_dotenv()
12
+
13
+ YOUTUBE_API_KEY = os.getenv(
14
+ "GEMINI_API_KEY"
15
+ ) # For using Gemini API, reuse this api key
16
+
17
+
18
+ def extract_limited_content(html_content: str) -> Optional[str]:
19
+ """
20
+ Extracts limited key text content from HTML (title, meta description, and initial content).
21
+
22
+ Args:
23
+ html_content (str): The HTML content as a string.
24
+
25
+ Returns:
26
+ str: The extracted text content, or None if extraction fails.
27
+ """
28
+ try:
29
+ soup = BeautifulSoup(html_content, "html.parser")
30
+
31
+ title = soup.title.string if soup.title else ""
32
+ meta_description = soup.find("meta", attrs={"name": "description"})
33
+ meta_description = (
34
+ meta_description.get("content", "") if meta_description else ""
35
+ )
36
+ main_content_elements = soup.find_all(["p", "h1", "h2", "h3", "h4"])
37
+ main_content = " ".join(
38
+ element.get_text(separator=" ", strip=True)
39
+ for element in main_content_elements[:5]
40
+ )
41
+
42
+ return f"{title}. {meta_description}. {main_content}"
43
+ except Exception as e:
44
+ print(f"Error extracting content from HTML: {e}")
45
+ return None
46
+
47
+
48
+ def fetch_url_content(url: str) -> Optional[str]:
49
+ """
50
+ Fetches the HTML content from a given URL.
51
+
52
+ Args:
53
+ url (str): The URL to fetch.
54
+
55
+ Returns:
56
+ str: The HTML content or None if the request fails.
57
+ """
58
+ try:
59
+ response = requests.get(url, timeout=10)
60
+ response.raise_for_status() # Raise an exception for bad status codes
61
+ return response.text
62
+ except requests.exceptions.RequestException as e:
63
+ print(f"Error fetching URL {url}: {e}")
64
+ return None
65
+
66
+
67
+ def extract_youtube_data(video_id: str) -> Optional[str]:
68
+ """
69
+ Extracts the title and description from a YouTube video.
70
+
71
+ Args:
72
+ video_id (str): The YouTube video ID.
73
+
74
+ Returns:
75
+ str: The title and description of the youtube video or None if something goes wrong.
76
+ """
77
+ try:
78
+ youtube = build("youtube", "v3", developerKey=YOUTUBE_API_KEY)
79
+ request = youtube.videos().list(part="snippet", id=video_id)
80
+ response = request.execute()
81
+ if not response or "items" not in response or not response["items"]:
82
+ return None
83
+ video_data = response["items"][0]["snippet"]
84
+ title = video_data.get("title", "")
85
+ description = video_data.get("description", "")
86
+ return f"{title}. {description}"
87
+ except HttpError as e:
88
+ print(f"An HTTP error {e.resp.status} occurred:\n{e.content}")
89
+ return None
90
+ except Exception as e:
91
+ print(f"An Error occurred: {e}")
92
+ return None
93
+
94
+
95
+ def get_content_from_url(url: str) -> Optional[str]:
96
+ """
97
+ Helper function to extract content from both normal urls and youtube urls.
98
+
99
+ Args:
100
+ url (str): url to get content from
101
+
102
+ Returns:
103
+ str: The content of the url or None.
104
+ """
105
+ if "youtube.com" in url or "youtu.be" in url:
106
+ video_id = get_youtube_id(url)
107
+ if video_id:
108
+ return extract_youtube_data(video_id)
109
+ return None
110
+ html_content = fetch_url_content(url)
111
+ if html_content:
112
+ return extract_limited_content(
113
+ html_content
114
+ ) # Changed this to extract limited content
115
+ return None
116
+
117
+
118
+ def get_youtube_id(url: str) -> Optional[str]:
119
+ """
120
+ Extracts the YouTube video ID from a URL.
121
+
122
+ Args:
123
+ url (str): The URL of the YouTube video.
124
+
125
+ Returns:
126
+ str: The video ID or None if not found
127
+ """
128
+ try:
129
+ if "youtube.com" in url:
130
+ from urllib.parse import urlparse, parse_qs
131
+
132
+ parsed_url = urlparse(url)
133
+ if parsed_url.query:
134
+ query_params = parse_qs(parsed_url.query)
135
+ if "v" in query_params:
136
+ return query_params["v"][0]
137
+ if parsed_url.path.startswith("/watch/"):
138
+ return parsed_url.path.split("/")[2]
139
+ elif "youtu.be" in url:
140
+ from urllib.parse import urlparse
141
+
142
+ parsed_url = urlparse(url)
143
+ return parsed_url.path[1:]
144
+ return None
145
+ except Exception as e:
146
+ print(f"Error extracting youtube id: {e}")
147
+ return None
148
+
149
+
150
+ if __name__ == "__main__":
151
+ sample_url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
152
+ content = get_content_from_url(sample_url)
153
+ if content:
154
+ print(f"Content of url : {content[:200]} ...")
155
+ else:
156
+ print("Error fetching content")
@@ -0,0 +1,75 @@
1
+ import os
2
+ import asyncio
3
+ import argparse
4
+ from dotenv import load_dotenv, set_key
5
+ from .categorizer import main_categorizer
6
+
7
+ load_dotenv()
8
+
9
+
10
+ async def main():
11
+ # Create argument parser for CLI usage
12
+ parser = argparse.ArgumentParser(description="Brave Tab Manager CLI Tool")
13
+ parser.add_argument(
14
+ "-c",
15
+ "--categorize",
16
+ action="store_true",
17
+ help="Categorize and save current open tabs.",
18
+ )
19
+ parser.add_argument("-v", "--version", action="version", version="%(prog)s 0.1.0")
20
+ parser.add_argument(
21
+ "-m",
22
+ "--model",
23
+ type=str,
24
+ choices=["gemini", "mistral", "ollama"],
25
+ default="gemini",
26
+ help="Specify the LLM model for categorization",
27
+ )
28
+ parser.add_argument(
29
+ "--save-keys",
30
+ action="store_true",
31
+ help="Save API keys to the .env file. Use this option with -gk or -mk to set the keys.",
32
+ )
33
+ parser.add_argument("-mk", "--mistral-key", type=str, help="Mistral AI API key.")
34
+ parser.add_argument("-gk", "--gemini-key", type=str, help="Google Gemini API key.")
35
+ parser.add_argument(
36
+ "-om",
37
+ "--ollama-model",
38
+ type=str,
39
+ default="llama3.2",
40
+ help="Ollama model to use for categorization (default: llama3.2)",
41
+ )
42
+ args = parser.parse_args()
43
+
44
+ # If we're saving keys, update environment and .env file
45
+ if args.save_keys:
46
+ env_path = ".env"
47
+ if args.gemini_key:
48
+ os.environ["GEMINI_API_KEY"] = args.gemini_key
49
+ set_key(env_path, "GEMINI_API_KEY", args.gemini_key)
50
+ print("Gemini key has been set")
51
+ if args.mistral_key:
52
+ os.environ["MISTRAL_API_KEY"] = args.mistral_key
53
+ set_key(env_path, "MISTRAL_API_KEY", args.mistral_key)
54
+ print("Mistral key has been set")
55
+
56
+ # Run categorization if requested
57
+ if args.categorize:
58
+ await main_categorizer(
59
+ args.model,
60
+ args.save_keys,
61
+ args.mistral_key,
62
+ args.gemini_key,
63
+ args.ollama_model,
64
+ )
65
+ elif not args.save_keys and not getattr(args, "version", False):
66
+ parser.print_help()
67
+
68
+
69
+ def entry_point():
70
+ # Wrap async main in a synchronous entry point
71
+ asyncio.run(main())
72
+
73
+
74
+ if __name__ == "__main__":
75
+ entry_point()
@@ -0,0 +1,60 @@
1
+ import asyncio
2
+ from playwright.async_api import async_playwright
3
+
4
+ BRAVE_DEBUG_PORT = 9222 # Define the debug port here
5
+
6
+
7
+ async def get_browser_tabs(browser_url: str):
8
+ """
9
+ Retrieves a list of open tabs from the browser.
10
+
11
+ Args:
12
+ browser_url (str): The browser's http url.
13
+
14
+ Returns:
15
+ list[dict]: A list of dictionaries, each containing 'title' and 'url' keys.
16
+ Returns empty list if it fails to connect to browser.
17
+ """
18
+ try:
19
+ async with async_playwright() as p:
20
+ browser = await p.chromium.connect_over_cdp(browser_url)
21
+
22
+ tabs = []
23
+ for context in browser.contexts: # Iterate through contexts
24
+ for page in context.pages: # Iterate through pages in each context
25
+ title = await page.title()
26
+ url = page.url
27
+ tabs.append({"title": title, "url": url})
28
+ await browser.close()
29
+ return tabs
30
+ except Exception as e:
31
+ print(f"Failed to get browser tabs: {e}")
32
+ return []
33
+
34
+
35
+ async def get_brave_tabs():
36
+ """
37
+ Helper function to get brave browser's http url using debug port.
38
+ Returns:
39
+ list[dict]: A list of dictionaries, each containing 'title' and 'url' keys.
40
+ Returns empty list if it fails to get browser tabs.
41
+ """
42
+ try:
43
+ browser_url = f"http://127.0.0.1:{BRAVE_DEBUG_PORT}"
44
+ return await get_browser_tabs(browser_url)
45
+ except Exception as e:
46
+ print(f"Failed to get browser tabs: {e}")
47
+ return []
48
+
49
+
50
+ if __name__ == "__main__":
51
+
52
+ async def main():
53
+ tabs = await get_brave_tabs()
54
+ if tabs:
55
+ for tab in tabs:
56
+ print(f"Title: {tab['title']}, URL: {tab['url']}")
57
+ else:
58
+ print("No tabs found.")
59
+
60
+ asyncio.run(main())
@@ -0,0 +1,151 @@
1
+ import os
2
+ import json
3
+ import logging
4
+ from datetime import datetime
5
+ from typing import List, Dict
6
+
7
+ # Silence the google-generativeai logs, has to be done before the config import
8
+ logging.getLogger("google.generativeai").setLevel(logging.ERROR)
9
+
10
+
11
+ def save_tabs_to_json(tabs: List[Dict[str, str]], data_dir: str = "data") -> str:
12
+ """
13
+ Saves tab data to a JSON file.
14
+
15
+ Args:
16
+ tabs (list): A list of dictionaries, each with 'title', 'url', 'main_category', and 'tags' keys.
17
+ data_dir (str): The directory to save the JSON file in
18
+
19
+ Returns:
20
+ str: The path to the saved JSON file
21
+ """
22
+ now = datetime.now()
23
+ date_str = now.strftime("%d-%m-%Y")
24
+ time_str = now.strftime("%H-%M-%S")
25
+ day_str = now.strftime("%A")
26
+ filename = f"tabs_{date_str}_{day_str}_{time_str}.json"
27
+
28
+ dated_dir = os.path.join(data_dir, date_str)
29
+ filepath = os.path.join(dated_dir, filename)
30
+
31
+ os.makedirs(dated_dir, exist_ok=True)
32
+
33
+ with open(filepath, "w", encoding="utf-8") as f:
34
+ json.dump(tabs, f, indent=4)
35
+
36
+ all_tabs_filepath = os.path.join(data_dir, "all_tabs.md")
37
+ append_tabs_to_all_markdown(tabs, all_tabs_filepath)
38
+ return filepath
39
+
40
+
41
+ def append_tabs_to_all_markdown(tabs: List[Dict[str, str]], filepath: str):
42
+ """
43
+ Appends tab data to the central all_tabs.md file.
44
+
45
+ Args:
46
+ tabs(list): A list of dictionaries, each with 'title', 'url', 'main_category', and 'tags' keys.
47
+ filepath(str): The path of the all_tabs.md file
48
+ """
49
+ with open(filepath, "a", encoding="utf-8") as f:
50
+ f.write("# Browser Tab Data\n\n")
51
+ for tab in tabs:
52
+ title = tab.get("title", "No Title")
53
+ url = tab.get("url", "No URL")
54
+ main_category = tab.get("main_category", "Other")
55
+ tags = tab.get("tags", [])
56
+
57
+ f.write(f"## {title}\n")
58
+ f.write(f"- **URL:** {url}\n")
59
+ f.write(f"- **Main Category:** {main_category}\n")
60
+ f.write(f"- **Tags:** {', '.join(tags)}\n\n")
61
+
62
+
63
+ def convert_json_to_markdown(json_filepath: str, data_dir: str = "data") -> str:
64
+ """
65
+ Converts the saved JSON data to markdown format.
66
+
67
+ Args:
68
+ json_filepath(str): The path of the json file
69
+ data_dir(str): The directory where data is stored (used to get the markdown output folder path)
70
+
71
+ Returns:
72
+ str: The path of the created markdown file
73
+ """
74
+ with open(json_filepath, "r", encoding="utf-8") as f:
75
+ tabs = json.load(f)
76
+
77
+ now = datetime.now()
78
+ date_str = now.strftime("%d-%m-%Y")
79
+ time_str = now.strftime("%H-%M-%S")
80
+ day_str = now.strftime("%A")
81
+ markdown_dir = os.path.join(data_dir, date_str)
82
+ os.makedirs(markdown_dir, exist_ok=True)
83
+ markdown_file = os.path.join(
84
+ markdown_dir, f"tabs_{date_str}_{day_str}_{time_str}.md"
85
+ )
86
+ with open(markdown_file, "w", encoding="utf-8") as f:
87
+ f.write(f"# Tabs - {date_str} ({day_str})\n\n")
88
+ for tab in tabs:
89
+ title = tab.get("title", "No Title")
90
+ url = tab.get("url", "No URL")
91
+ main_category = tab.get("main_category", "Other")
92
+ tags = tab.get("tags", [])
93
+
94
+ f.write(f"## {title}\n")
95
+ f.write(f"- **URL:** {url}\n")
96
+ f.write(f"- **Main Category:** {main_category}\n")
97
+ f.write(f"- **Tags:** {', '.join(tags)}\n\n")
98
+ return markdown_file
99
+
100
+
101
+ if __name__ == "__main__":
102
+
103
+ # Testing the functions
104
+ sample_tabs = [
105
+ {
106
+ "title": "Playwright Docs",
107
+ "url": "https://playwright.dev/docs/api/class-browsertype#browser-type-connect-over-cdp",
108
+ "main_category": "Other",
109
+ "tags": [
110
+ "Playwright",
111
+ "Browsertype",
112
+ "Connect",
113
+ "Browser Instance",
114
+ "Automation",
115
+ ],
116
+ },
117
+ {
118
+ "title": "Rick Astley - Never Gonna Give You Up",
119
+ "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
120
+ "main_category": "YouTube",
121
+ "tags": [
122
+ "Music Video",
123
+ "Rick Astley",
124
+ "Official",
125
+ "Never Gonna Give You Up",
126
+ "1980s",
127
+ ],
128
+ },
129
+ {
130
+ "title": "Brave Tab Manager: Design Plan",
131
+ "url": "https://aistudio.google.com/u/3/prompts/1SyHQU8Re8fJ8Cga95N5jG5ptRmvV8BoL",
132
+ "main_category": "Other",
133
+ "tags": [
134
+ "Sign In",
135
+ "Guest Mode",
136
+ "Private Browsing",
137
+ "Forgot Email",
138
+ "Google Accounts",
139
+ ],
140
+ },
141
+ {
142
+ "title": "Github OAuth",
143
+ "url": "https://github.com/login/oauth/authorize?client_id=01ab8ac9400c4e429b23&redirect_uri=https%3A%2F%2Fvscode.dev%2Fredirect&scope=user%3Aemail&skip_account_picker=true&state=vscode%253A%252F%2Fvscode.github-authentication%252Fdid-authenticate%253Fnonce%253Db9147fedcf4304bb%2526windowId%253D1",
144
+ "main_category": "Coding",
145
+ "tags": ["Projects", "Software", "Passkey", "Coding", "Github"],
146
+ },
147
+ ]
148
+ json_file = save_tabs_to_json(sample_tabs)
149
+ print(f"Saved tabs to JSON: {json_file}")
150
+ markdown_file = convert_json_to_markdown(json_file)
151
+ print(f"Converted JSON to markdown: {markdown_file}")
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.1
2
+ Name: tabman
3
+ Version: 0.1.0
4
+ Home-page: https://github.com/Anshulgada/brave-tab-manager
5
+ Author: Anshul Gada
6
+ License-File: LICENSE
7
+ Requires-Dist: playwright
8
+ Requires-Dist: requests
9
+ Requires-Dist: beautifulsoup4
10
+ Requires-Dist: google-generativeai
11
+ Requires-Dist: google-api-python-client
12
+ Requires-Dist: tenacity
13
+ Requires-Dist: openai
14
+ Requires-Dist: python-dotenv
15
+ Requires-Dist: ollama
16
+ Requires-Dist: markdown
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ setup.py
4
+ tabman/__init__.py
5
+ tabman/categorizer.py
6
+ tabman/content_fetcher.py
7
+ tabman/main.py
8
+ tabman/tab_capture.py
9
+ tabman/tab_saver.py
10
+ tabman.egg-info/PKG-INFO
11
+ tabman.egg-info/SOURCES.txt
12
+ tabman.egg-info/dependency_links.txt
13
+ tabman.egg-info/entry_points.txt
14
+ tabman.egg-info/requires.txt
15
+ tabman.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tabman = tabman.main:entry_point
@@ -0,0 +1,10 @@
1
+ playwright
2
+ requests
3
+ beautifulsoup4
4
+ google-generativeai
5
+ google-api-python-client
6
+ tenacity
7
+ openai
8
+ python-dotenv
9
+ ollama
10
+ markdown
@@ -0,0 +1 @@
1
+ tabman