wrkmon 1.0.1__py3-none-any.whl

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.
wrkmon/cli.py ADDED
@@ -0,0 +1,289 @@
1
+ """CLI entry point for wrkmon using Typer."""
2
+
3
+ import asyncio
4
+ from typing import Optional
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+
10
+ from wrkmon import __version__
11
+
12
+ app = typer.Typer(
13
+ name="wrkmon",
14
+ help="Work Monitor - A developer productivity tool",
15
+ no_args_is_help=False,
16
+ )
17
+ console = Console()
18
+
19
+
20
+ def version_callback(value: bool) -> None:
21
+ """Show version and exit."""
22
+ if value:
23
+ console.print(f"wrkmon version {__version__}")
24
+ raise typer.Exit()
25
+
26
+
27
+ @app.callback(invoke_without_command=True)
28
+ def main(
29
+ ctx: typer.Context,
30
+ version: bool = typer.Option(
31
+ False,
32
+ "--version",
33
+ "-v",
34
+ callback=version_callback,
35
+ is_eager=True,
36
+ help="Show version and exit",
37
+ ),
38
+ ) -> None:
39
+ """
40
+ Work Monitor - Monitor and manage system processes.
41
+
42
+ Run without arguments to launch the interactive monitor.
43
+ """
44
+ if ctx.invoked_subcommand is None:
45
+ # Launch TUI
46
+ from wrkmon.app import run_app
47
+ run_app()
48
+
49
+
50
+ @app.command()
51
+ def search(
52
+ query: str = typer.Argument(..., help="Search query"),
53
+ limit: int = typer.Option(10, "--limit", "-l", help="Maximum results"),
54
+ ) -> None:
55
+ """Search for processes to monitor."""
56
+ from wrkmon.core.youtube import YouTubeClient
57
+ from wrkmon.utils.stealth import get_stealth
58
+
59
+ stealth = get_stealth()
60
+
61
+ async def do_search():
62
+ client = YouTubeClient()
63
+ results = await client.search(query, max_results=limit)
64
+ return results
65
+
66
+ console.print(f"[dim]Searching for: {query}[/dim]\n")
67
+
68
+ results = asyncio.run(do_search())
69
+
70
+ if not results:
71
+ console.print("[yellow]No results found[/yellow]")
72
+ return
73
+
74
+ table = Table(title="Search Results")
75
+ table.add_column("#", style="dim")
76
+ table.add_column("Process Name")
77
+ table.add_column("PID", style="dim")
78
+ table.add_column("Duration")
79
+ table.add_column("Status", style="green")
80
+
81
+ for i, result in enumerate(results, 1):
82
+ process_name = stealth.get_fake_process_name(result.title)
83
+ fake_pid = str(stealth.get_fake_pid())
84
+ duration = result.duration_str
85
+ table.add_row(str(i), process_name[:50], fake_pid, duration, "READY")
86
+
87
+ console.print(table)
88
+ console.print(f"\n[dim]Use 'wrkmon play <id>' to start a process[/dim]")
89
+
90
+
91
+ @app.command()
92
+ def play(
93
+ video_id: str = typer.Argument(..., help="Video ID or URL to play"),
94
+ ) -> None:
95
+ """Play a specific process by ID or URL."""
96
+ import re
97
+
98
+ # Extract video ID from URL if needed
99
+ if "youtube.com" in video_id or "youtu.be" in video_id:
100
+ # Extract ID from URL
101
+ patterns = [
102
+ r"(?:v=|/)([a-zA-Z0-9_-]{11})",
103
+ r"youtu\.be/([a-zA-Z0-9_-]{11})",
104
+ ]
105
+ for pattern in patterns:
106
+ match = re.search(pattern, video_id)
107
+ if match:
108
+ video_id = match.group(1)
109
+ break
110
+
111
+ from wrkmon.core.youtube import YouTubeClient
112
+ from wrkmon.core.player import AudioPlayer
113
+ from wrkmon.core.cache import Cache
114
+ from wrkmon.utils.stealth import get_stealth
115
+
116
+ stealth = get_stealth()
117
+ cache = Cache()
118
+
119
+ async def do_play():
120
+ client = YouTubeClient()
121
+ player = AudioPlayer()
122
+
123
+ # Get stream info
124
+ console.print("[dim]Getting process info...[/dim]")
125
+
126
+ # Check cache
127
+ cached = cache.get(video_id)
128
+ if cached:
129
+ audio_url = cached.audio_url
130
+ title = cached.title
131
+ else:
132
+ stream_info = await client.get_stream_url(video_id)
133
+ if not stream_info:
134
+ console.print("[red]Failed to get process info[/red]")
135
+ return False
136
+
137
+ audio_url = stream_info.audio_url
138
+ title = stream_info.title
139
+
140
+ # Cache it
141
+ cache.set(
142
+ video_id=video_id,
143
+ title=stream_info.title,
144
+ channel=stream_info.channel,
145
+ duration=stream_info.duration,
146
+ audio_url=audio_url,
147
+ )
148
+
149
+ # Start player
150
+ console.print("[dim]Starting process...[/dim]")
151
+ if not await player.start():
152
+ console.print("[red]Failed to start player[/red]")
153
+ return False
154
+
155
+ # Play
156
+ if await player.play(audio_url):
157
+ process_name = stealth.get_fake_process_name(title)
158
+ console.print(f"[green]Running:[/green] {process_name}")
159
+ console.print("[dim]Press Ctrl+C to stop[/dim]")
160
+
161
+ # Keep running until interrupted
162
+ try:
163
+ while True:
164
+ await asyncio.sleep(1)
165
+ except KeyboardInterrupt:
166
+ console.print("\n[yellow]Stopping process...[/yellow]")
167
+ finally:
168
+ await player.shutdown()
169
+
170
+ return True
171
+ else:
172
+ console.print("[red]Failed to start process[/red]")
173
+ return False
174
+
175
+ asyncio.run(do_play())
176
+
177
+
178
+ @app.command()
179
+ def queue() -> None:
180
+ """Show the current process queue."""
181
+ # For CLI, we show an empty queue message since queue is session-based
182
+ console.print("[dim]No processes in queue[/dim]")
183
+ console.print("[dim]Run 'wrkmon' to use the interactive monitor[/dim]")
184
+
185
+
186
+ @app.command()
187
+ def history(
188
+ limit: int = typer.Option(20, "--limit", "-l", help="Maximum entries to show"),
189
+ ) -> None:
190
+ """Show process history."""
191
+ from wrkmon.data.database import Database
192
+ from wrkmon.utils.stealth import get_stealth
193
+
194
+ stealth = get_stealth()
195
+ db = Database()
196
+
197
+ entries = db.get_history(limit=limit)
198
+
199
+ if not entries:
200
+ console.print("[dim]No history yet[/dim]")
201
+ return
202
+
203
+ table = Table(title="Process History")
204
+ table.add_column("#", style="dim")
205
+ table.add_column("Process Name")
206
+ table.add_column("Duration")
207
+ table.add_column("Runs", style="cyan")
208
+ table.add_column("Last Run", style="dim")
209
+
210
+ for i, entry in enumerate(entries, 1):
211
+ process_name = stealth.get_fake_process_name(entry.track.title)
212
+ duration = entry.track.duration_str
213
+ runs = str(entry.play_count)
214
+ last_run = entry.played_at.strftime("%Y-%m-%d %H:%M")
215
+ table.add_row(str(i), process_name[:40], duration, runs, last_run)
216
+
217
+ console.print(table)
218
+ db.close()
219
+
220
+
221
+ @app.command()
222
+ def playlists() -> None:
223
+ """List all playlists."""
224
+ from wrkmon.data.database import Database
225
+
226
+ db = Database()
227
+ playlists = db.get_all_playlists()
228
+
229
+ if not playlists:
230
+ console.print("[dim]No playlists yet[/dim]")
231
+ console.print("[dim]Run 'wrkmon' to create playlists[/dim]")
232
+ return
233
+
234
+ table = Table(title="Playlists")
235
+ table.add_column("#", style="dim")
236
+ table.add_column("Name")
237
+ table.add_column("Tracks", style="cyan")
238
+ table.add_column("Created", style="dim")
239
+
240
+ for i, playlist in enumerate(playlists, 1):
241
+ created = playlist.created_at.strftime("%Y-%m-%d") if playlist.created_at else "N/A"
242
+ table.add_row(str(i), playlist.name, str(playlist.track_count), created)
243
+
244
+ console.print(table)
245
+ db.close()
246
+
247
+
248
+ @app.command()
249
+ def clear_cache() -> None:
250
+ """Clear the URL cache."""
251
+ from wrkmon.core.cache import Cache
252
+
253
+ cache = Cache()
254
+ stats = cache.get_stats()
255
+ count = cache.clear()
256
+
257
+ console.print(f"[green]Cleared {count} cached entries[/green]")
258
+
259
+
260
+ @app.command()
261
+ def clear_history() -> None:
262
+ """Clear play history."""
263
+ from wrkmon.data.database import Database
264
+
265
+ db = Database()
266
+ count = db.clear_history()
267
+ db.close()
268
+
269
+ console.print(f"[green]Cleared {count} history entries[/green]")
270
+
271
+
272
+ @app.command()
273
+ def config() -> None:
274
+ """Show configuration info."""
275
+ from wrkmon.utils.config import get_config
276
+
277
+ cfg = get_config()
278
+
279
+ console.print("[bold]Configuration[/bold]\n")
280
+ console.print(f"Config directory: {cfg.config_dir}")
281
+ console.print(f"Data directory: {cfg.data_dir}")
282
+ console.print(f"Database: {cfg.database_path}")
283
+ console.print(f"Cache: {cfg.cache_path}")
284
+ console.print(f"\nVolume: {cfg.volume}%")
285
+ console.print(f"Cache TTL: {cfg.url_ttl_hours} hours")
286
+
287
+
288
+ if __name__ == "__main__":
289
+ app()
@@ -0,0 +1,8 @@
1
+ """Core functionality for wrkmon."""
2
+
3
+ from wrkmon.core.youtube import YouTubeClient
4
+ from wrkmon.core.player import AudioPlayer
5
+ from wrkmon.core.queue import PlayQueue
6
+ from wrkmon.core.cache import Cache
7
+
8
+ __all__ = ["YouTubeClient", "AudioPlayer", "PlayQueue", "Cache"]
wrkmon/core/cache.py ADDED
@@ -0,0 +1,208 @@
1
+ """Audio URL and metadata caching."""
2
+
3
+ import json
4
+ import sqlite3
5
+ import time
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ from wrkmon.utils.config import get_config
11
+
12
+
13
+ @dataclass
14
+ class CachedStream:
15
+ """Cached stream information."""
16
+
17
+ video_id: str
18
+ title: str
19
+ channel: str
20
+ duration: int
21
+ audio_url: str
22
+ thumbnail_url: Optional[str]
23
+ cached_at: float
24
+ expires_at: float
25
+
26
+ @property
27
+ def is_expired(self) -> bool:
28
+ """Check if cache entry is expired."""
29
+ return time.time() > self.expires_at
30
+
31
+
32
+ class Cache:
33
+ """Manages caching of audio URLs and metadata."""
34
+
35
+ def __init__(self, db_path: Optional[Path] = None):
36
+ config = get_config()
37
+ self._db_path = db_path or config.cache_path
38
+ self._ttl_hours = config.url_ttl_hours
39
+ self._max_entries = config.get("cache", "max_entries", 1000)
40
+ self._init_db()
41
+
42
+ def _init_db(self) -> None:
43
+ """Initialize the cache database."""
44
+ self._db_path.parent.mkdir(parents=True, exist_ok=True)
45
+
46
+ with sqlite3.connect(self._db_path) as conn:
47
+ conn.execute("""
48
+ CREATE TABLE IF NOT EXISTS stream_cache (
49
+ video_id TEXT PRIMARY KEY,
50
+ title TEXT NOT NULL,
51
+ channel TEXT NOT NULL,
52
+ duration INTEGER NOT NULL,
53
+ audio_url TEXT NOT NULL,
54
+ thumbnail_url TEXT,
55
+ cached_at REAL NOT NULL,
56
+ expires_at REAL NOT NULL
57
+ )
58
+ """)
59
+ conn.execute("""
60
+ CREATE INDEX IF NOT EXISTS idx_expires_at ON stream_cache(expires_at)
61
+ """)
62
+ conn.commit()
63
+
64
+ def get(self, video_id: str) -> Optional[CachedStream]:
65
+ """Get a cached stream by video ID."""
66
+ with sqlite3.connect(self._db_path) as conn:
67
+ conn.row_factory = sqlite3.Row
68
+ cursor = conn.execute(
69
+ """
70
+ SELECT * FROM stream_cache
71
+ WHERE video_id = ? AND expires_at > ?
72
+ """,
73
+ (video_id, time.time()),
74
+ )
75
+ row = cursor.fetchone()
76
+
77
+ if row:
78
+ return CachedStream(
79
+ video_id=row["video_id"],
80
+ title=row["title"],
81
+ channel=row["channel"],
82
+ duration=row["duration"],
83
+ audio_url=row["audio_url"],
84
+ thumbnail_url=row["thumbnail_url"],
85
+ cached_at=row["cached_at"],
86
+ expires_at=row["expires_at"],
87
+ )
88
+
89
+ return None
90
+
91
+ def set(
92
+ self,
93
+ video_id: str,
94
+ title: str,
95
+ channel: str,
96
+ duration: int,
97
+ audio_url: str,
98
+ thumbnail_url: Optional[str] = None,
99
+ ) -> CachedStream:
100
+ """Cache a stream URL."""
101
+ now = time.time()
102
+ expires_at = now + (self._ttl_hours * 3600)
103
+
104
+ with sqlite3.connect(self._db_path) as conn:
105
+ conn.execute(
106
+ """
107
+ INSERT OR REPLACE INTO stream_cache
108
+ (video_id, title, channel, duration, audio_url, thumbnail_url, cached_at, expires_at)
109
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
110
+ """,
111
+ (video_id, title, channel, duration, audio_url, thumbnail_url, now, expires_at),
112
+ )
113
+ conn.commit()
114
+
115
+ # Cleanup old entries if we're over the limit
116
+ self._cleanup()
117
+
118
+ return CachedStream(
119
+ video_id=video_id,
120
+ title=title,
121
+ channel=channel,
122
+ duration=duration,
123
+ audio_url=audio_url,
124
+ thumbnail_url=thumbnail_url,
125
+ cached_at=now,
126
+ expires_at=expires_at,
127
+ )
128
+
129
+ def delete(self, video_id: str) -> bool:
130
+ """Delete a cache entry."""
131
+ with sqlite3.connect(self._db_path) as conn:
132
+ cursor = conn.execute(
133
+ "DELETE FROM stream_cache WHERE video_id = ?",
134
+ (video_id,),
135
+ )
136
+ conn.commit()
137
+ return cursor.rowcount > 0
138
+
139
+ def clear(self) -> int:
140
+ """Clear all cache entries. Returns count deleted."""
141
+ with sqlite3.connect(self._db_path) as conn:
142
+ cursor = conn.execute("DELETE FROM stream_cache")
143
+ conn.commit()
144
+ return cursor.rowcount
145
+
146
+ def clear_expired(self) -> int:
147
+ """Clear expired cache entries. Returns count deleted."""
148
+ with sqlite3.connect(self._db_path) as conn:
149
+ cursor = conn.execute(
150
+ "DELETE FROM stream_cache WHERE expires_at <= ?",
151
+ (time.time(),),
152
+ )
153
+ conn.commit()
154
+ return cursor.rowcount
155
+
156
+ def _cleanup(self) -> None:
157
+ """Cleanup old entries if over the limit."""
158
+ with sqlite3.connect(self._db_path) as conn:
159
+ # First clear expired
160
+ conn.execute(
161
+ "DELETE FROM stream_cache WHERE expires_at <= ?",
162
+ (time.time(),),
163
+ )
164
+
165
+ # Then check count
166
+ cursor = conn.execute("SELECT COUNT(*) FROM stream_cache")
167
+ count = cursor.fetchone()[0]
168
+
169
+ if count > self._max_entries:
170
+ # Delete oldest entries
171
+ excess = count - self._max_entries
172
+ conn.execute(
173
+ """
174
+ DELETE FROM stream_cache
175
+ WHERE video_id IN (
176
+ SELECT video_id FROM stream_cache
177
+ ORDER BY cached_at ASC
178
+ LIMIT ?
179
+ )
180
+ """,
181
+ (excess,),
182
+ )
183
+
184
+ conn.commit()
185
+
186
+ def get_stats(self) -> dict:
187
+ """Get cache statistics."""
188
+ with sqlite3.connect(self._db_path) as conn:
189
+ cursor = conn.execute("SELECT COUNT(*) FROM stream_cache")
190
+ total = cursor.fetchone()[0]
191
+
192
+ cursor = conn.execute(
193
+ "SELECT COUNT(*) FROM stream_cache WHERE expires_at > ?",
194
+ (time.time(),),
195
+ )
196
+ valid = cursor.fetchone()[0]
197
+
198
+ cursor = conn.execute(
199
+ "SELECT SUM(LENGTH(audio_url)) FROM stream_cache"
200
+ )
201
+ size = cursor.fetchone()[0] or 0
202
+
203
+ return {
204
+ "total_entries": total,
205
+ "valid_entries": valid,
206
+ "expired_entries": total - valid,
207
+ "approximate_size_bytes": size,
208
+ }