Skip to content

Commit 990e120

Browse files
authored
feat: add icon extraction feature (#192)
2 parents 2220ba5 + f44ea10 commit 990e120

26 files changed

+3685
-391
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# Changelog
22
All notable changes to this project will be documented in this file.
33

4+
## v1.2.0-alpha
5+
### CHANGES
6+
This release implement new icon extraction feature and implement new verfication service.
7+
8+
49
## v1.1.3-alpha
510
## v1.1.2-alpha
611
## v1.1.0-alpha

docs/wiki.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ my-unicorn list --available
6161

6262
# Remove apps
6363
my-unicorn remove appflowy --keep-config
64+
my-unicorn remove appflowy qownotes
6465

6566
# Show configuration
6667
my-unicorn config --show

my_unicorn/catalog/dummyapp.json

Lines changed: 0 additions & 1 deletion
This file was deleted.

my_unicorn/catalog/qownnotes.json

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44
"appimage": {
55
"rename": "qownnotes",
66
"name_template": "{repo}-{characteristic_suffix}.AppImage",
7-
"characteristic_suffix": [
8-
"x86_64.AppImage"
9-
]
7+
"characteristic_suffix": ["x86_64.AppImage"]
108
},
119
"github": {
1210
"repo": true,
@@ -20,6 +18,7 @@
2018
},
2119
"icon": {
2220
"url": "https://raw.githubusercontent.com/pbek/QOwnNotes/develop/icons/icon.png",
23-
"name": "qownnotes.png"
21+
"name": "qownnotes.png",
22+
"extraction": true
2423
}
25-
}
24+
}

my_unicorn/commands/backup.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ async def _handle_cleanup(self, app_name: str | None = None) -> None:
287287
print(f"✅ Cleanup completed (keeping {max_backups} most recent backups)")
288288

289289
except Exception as e:
290-
logger.error(f"❌ Failed to cleanup backups: {e}")
290+
logger.error("❌ Failed to cleanup backups: %s", e)
291291

292292
async def _handle_info(self, app_name: str) -> None:
293293
"""Handle show backup info operation.
@@ -343,7 +343,7 @@ async def _handle_info(self, app_name: str) -> None:
343343
print(f" 🔄 Max backups kept: {max_backups if max_backups > 0 else 'unlimited'}")
344344

345345
except Exception as e:
346-
logger.error(f"❌ Failed to get backup info for {app_name}: {e}")
346+
logger.error("❌ Failed to get backup info for %s: %s", app_name, e)
347347

348348
async def _handle_migrate(self) -> None:
349349
"""Handle migration of old backup format."""
@@ -358,4 +358,4 @@ async def _handle_migrate(self) -> None:
358358
print("ℹ️ No old backup files found to migrate")
359359

360360
except Exception as e:
361-
logger.error(f"❌ Failed to migrate old backups: {e}")
361+
logger.error("❌ Failed to migrate old backups: %s", e)

my_unicorn/commands/install.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,13 @@ async def execute(self, targets: list[str], **options: Any) -> list[dict[str, An
9696
if not targets:
9797
raise ValidationError("No installation targets provided")
9898

99-
logger.info(f"🚀 Starting installation of {len(targets)} target(s)")
99+
logger.info("🚀 Starting installation of %d target(s)", len(targets))
100100

101101
# Separate targets into URLs and catalog apps
102102
url_targets, catalog_targets = self._separate_targets(targets)
103103

104104
# Prepare options with defaults
105-
#TODO: use the global setting for the concurrent option
105+
# TODO: use the global setting for the concurrent option
106106
install_options = {
107107
"concurrent": options.get("concurrent", 3),
108108
"show_progress": options.get("show_progress", True),
@@ -115,12 +115,12 @@ async def execute(self, targets: list[str], **options: Any) -> list[dict[str, An
115115
results = []
116116

117117
if url_targets:
118-
logger.info(f"📡 Installing {len(url_targets)} URL(s)")
118+
logger.info("📡 Installing %d URL(s)", len(url_targets))
119119
url_results = await self.url_strategy.install(url_targets, **install_options)
120120
results.extend(url_results)
121121

122122
if catalog_targets:
123-
logger.info(f"📚 Installing {len(catalog_targets)} catalog app(s)")
123+
logger.info("📚 Installing %d catalog app(s)", len(catalog_targets))
124124
catalog_results = await self.catalog_strategy.install(
125125
catalog_targets, **install_options
126126
)
@@ -195,7 +195,7 @@ def _print_installation_summary(self, results: list[dict[str, Any]]) -> None:
195195
logger.info("=" * 60)
196196

197197
if successful:
198-
logger.info(f"✅ Successfully installed ({len(successful)}):")
198+
logger.info("✅ Successfully installed (%d):", len(successful))
199199
for result in successful:
200200
target = result.get("target", "Unknown")
201201
path = result.get("path", "Unknown")
@@ -209,14 +209,16 @@ def _print_installation_summary(self, results: list[dict[str, Any]]) -> None:
209209

210210
version_info = f" v{version}" if version else ""
211211
source_info = f" [{source}]" if source != "unknown" else ""
212-
logger.info(f" • {target}{version_info}{path}{source_info}{status_info}")
212+
logger.info(
213+
" • %s%s → %s%s%s", target, version_info, path, source_info, status_info
214+
)
213215

214216
if failed:
215-
logger.info(f"\n❌ Failed installations ({len(failed)}):")
217+
logger.info("\n❌ Failed installations (%d):", len(failed))
216218
for result in failed:
217219
target = result.get("target", "Unknown")
218220
error = result.get("error", "Unknown error")
219-
logger.info(f" • {target}: {error}")
221+
logger.info(" • %s: %s", target, error)
220222

221223
logger.info("=" * 60)
222224

@@ -235,7 +237,7 @@ def validate_targets(self, targets: list[str]) -> None:
235237
except ValidationError:
236238
raise
237239

238-
logger.debug(f"✅ Validated {len(targets)} target(s)")
240+
logger.debug("✅ Validated %d target(s)", len(targets))
239241

240242
async def cleanup_failed_installations(self, results: list[dict[str, Any]]) -> None:
241243
"""Clean up any failed installations.
@@ -250,7 +252,7 @@ async def cleanup_failed_installations(self, results: list[dict[str, Any]]) -> N
250252
if path := result.get("path"):
251253
file_path = Path(path)
252254
if file_path.exists():
253-
logger.debug(f"🧹 Cleaning up failed installation: {file_path}")
255+
logger.debug("🧹 Cleaning up failed installation: %s", file_path)
254256
self.storage_service.remove_file(file_path)
255257

256258
def get_installation_stats(self, results: list[dict[str, Any]]) -> dict[str, int]:
@@ -349,10 +351,10 @@ async def execute(self, args: Namespace) -> None:
349351
)
350352

351353
except ValidationError as e:
352-
logger.error(f"❌ Validation error: {e}")
354+
logger.error("❌ Validation error: %s", e)
353355
logger.info("💡 Use 'list --available' to see available catalog apps.")
354356
except Exception as e:
355-
logger.error(f"❌ Installation failed: {e}")
357+
logger.error("❌ Installation failed: %s", e)
356358

357359
def _expand_comma_separated_targets(self, targets: list[str]) -> list[str]:
358360
"""Expand comma-separated targets into individual targets.

my_unicorn/commands/remove.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ async def _remove_single_app(self, app_name: str, keep_config: bool) -> None:
2626
app_config = self.config_manager.load_app_config(app_name)
2727
if not app_config:
2828
print(f"❌ App '{app_name}' not found")
29-
logger.debug(f"App config for '{app_name}' not found. Skipping removal.")
29+
logger.debug("App config for '%s' not found. Skipping removal.", app_name)
3030
return
3131

3232
# Remove AppImage files
@@ -46,7 +46,7 @@ async def _remove_single_app(self, app_name: str, keep_config: bool) -> None:
4646
print(f"✅ Kept config for {app_name}")
4747

4848
except Exception as e:
49-
logger.error(f"Failed to remove {app_name}: {e}", exc_info=True)
49+
logger.error("Failed to remove %s: %s", app_name, e, exc_info=True)
5050
print(f"❌ Failed to remove {app_name}: {e}")
5151

5252
def _remove_appimage_files(self, app_config: dict, app_name: str) -> None:
@@ -64,7 +64,7 @@ def _remove_appimage_files(self, app_config: dict, app_name: str) -> None:
6464
if path.exists():
6565
path.unlink()
6666
removed_files.append(str(path))
67-
logger.debug(f"Unlinked file: {path}")
67+
logger.debug("Removed file: %s", path)
6868

6969
if removed_files:
7070
print(f"✅ Removed AppImage(s): {', '.join(removed_files)}")
@@ -82,8 +82,8 @@ def _remove_desktop_entry(self, app_name: str) -> None:
8282
if remove_desktop_entry_for_app(app_name, self.config_manager):
8383
print(f"✅ Removed desktop entry for {app_name}")
8484
except Exception as e:
85-
logger.debug(f"Exception occurred while processing app '{app_name}': {e}")
86-
logger.warning(f"⚠️ Failed to remove desktop entry: {e}")
85+
logger.debug("Exception occurred while processing app '%s': %s", app_name, e)
86+
logger.warning("⚠️ Failed to remove desktop entry: %s", e)
8787

8888
def _remove_icon(self, app_config: dict) -> None:
8989
"""Remove icon file if icon config is present."""

my_unicorn/commands/update.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ async def execute(self, args: Namespace) -> None:
4040

4141
# Log the selected strategy for debugging
4242
strategy_name = UpdateStrategyFactory.get_strategy_name(context)
43-
logger.debug(f"Selected strategy: {strategy_name}")
43+
logger.debug("Selected strategy: %s", strategy_name)
4444

4545
# Validate inputs using the strategy's validation logic
4646
if not strategy.validate_inputs(context):
@@ -53,10 +53,10 @@ async def execute(self, args: Namespace) -> None:
5353
UpdateResultDisplay.display_summary(result)
5454

5555
# Log final result for debugging
56-
logger.debug(f"Update operation completed: {result.message}")
56+
logger.debug("Update operation completed: %s", result.message)
5757

5858
except Exception as e:
59-
logger.error(f"Update command failed: {e}")
59+
logger.error("Update command failed: %s", e)
6060
UpdateResultDisplay.display_error(f"Update operation failed: {e}")
6161

6262
def _build_context(self, args: Namespace) -> UpdateContext:

my_unicorn/desktop.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ def create_desktop_file(
250250
f"Desktop file content changed, updating: {desktop_file_path}"
251251
)
252252
else:
253-
logger.debug(f"Desktop file unchanged, skipping: {desktop_file_path}")
253+
logger.debug("Desktop file unchanged, skipping: %s", desktop_file_path)
254254
return desktop_file_path
255255
except OSError:
256256
# If we can't read the existing file, create a new one
@@ -266,9 +266,9 @@ def create_desktop_file(
266266
os.chmod(desktop_file_path, 0o755)
267267

268268
if desktop_file_path.exists():
269-
logger.info(f"🖥️ Updated desktop entry: {desktop_file_path.name}")
269+
logger.info("🖥️ Updated desktop entry: %s", desktop_file_path.name)
270270
else:
271-
logger.info(f"🖥️ Created desktop entry: {desktop_file_path.name}")
271+
logger.info("🖥️ Created desktop entry: %s", desktop_file_path.name)
272272
return desktop_file_path
273273

274274
except OSError as e:
@@ -303,7 +303,7 @@ def update_desktop_file(
303303
"""
304304
existing_file = self.find_existing_desktop_file()
305305
if not existing_file:
306-
logger.warning(f"No existing desktop file found for {self.app_name}")
306+
logger.warning("No existing desktop file found for %s", self.app_name)
307307
return None
308308

309309
# Update by recreating the file
@@ -319,11 +319,13 @@ def update_desktop_file(
319319
with open(existing_file, "w", encoding="utf-8") as f:
320320
f.write(content)
321321

322-
logger.debug(f"Updated desktop file: {existing_file}")
322+
logger.debug("Updated desktop file: %s", existing_file)
323323
return existing_file
324324

325325
except OSError as e:
326-
logger.error(f"Failed to update desktop file {existing_file}: {e}", exc_info=True)
326+
logger.error(
327+
"Failed to update desktop file %s: %s", existing_file, e, exc_info=True
328+
)
327329
return None
328330

329331
def remove_desktop_file(self) -> bool:
@@ -335,15 +337,17 @@ def remove_desktop_file(self) -> bool:
335337
"""
336338
existing_file = self.find_existing_desktop_file()
337339
if not existing_file:
338-
logger.debug(f"No desktop file found for {self.app_name}")
340+
logger.debug("No desktop file found for %s", self.app_name)
339341
return False
340342

341343
try:
342344
existing_file.unlink()
343-
logger.debug(f"Removed desktop file: {existing_file}")
345+
logger.debug("Removed desktop file: %s", existing_file)
344346
return True
345347
except OSError as e:
346-
logger.error(f"Failed to remove desktop file {existing_file}: {e}", exc_info=True)
348+
logger.error(
349+
"Failed to remove desktop file %s: %s", existing_file, e, exc_info=True
350+
)
347351
return False
348352

349353
def _is_browser_app(self) -> bool:
@@ -433,9 +437,9 @@ def parse_desktop_fields(content: str) -> dict[str, str]:
433437

434438
for field in important_fields:
435439
if existing_fields.get(field) != new_fields.get(field):
436-
logger.debug(f"Desktop file field changed: {field}")
437-
logger.debug(f" Old: {existing_fields.get(field)}")
438-
logger.debug(f" New: {new_fields.get(field)}")
440+
logger.debug("Desktop file field changed: %s", field)
441+
logger.debug(" Old: %s", existing_fields.get(field))
442+
logger.debug(" New: %s", new_fields.get(field))
439443
return True
440444

441445
return False
@@ -522,7 +526,7 @@ def refresh_desktop_database(self) -> bool:
522526
logger.debug("Desktop database refreshed")
523527
return True
524528
except Exception as e:
525-
logger.debug(f"Could not refresh desktop database: {e}")
529+
logger.debug("Could not refresh desktop database: %s", e)
526530

527531
return False
528532

0 commit comments

Comments
 (0)