harmony-plugin-api 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.
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: harmony-plugin-api
3
+ Version: 0.1.0
4
+ Summary: Pure plugin SDK for Harmony plugins
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+
8
+ # harmony-plugin-api
9
+
10
+ Pure plugin SDK for Harmony plugins.
11
+
12
+ This package only contains stable plugin-facing protocols, models, manifest parsing,
13
+ and registry spec types. It does not include Harmony host runtime implementations.
14
+
15
+ Host-specific theme, dialog, runtime, and bootstrap integrations must be provided by
16
+ the Harmony application and injected through `PluginContext`.
@@ -0,0 +1,9 @@
1
+ # harmony-plugin-api
2
+
3
+ Pure plugin SDK for Harmony plugins.
4
+
5
+ This package only contains stable plugin-facing protocols, models, manifest parsing,
6
+ and registry spec types. It does not include Harmony host runtime implementations.
7
+
8
+ Host-specific theme, dialog, runtime, and bootstrap integrations must be provided by
9
+ the Harmony application and injected through `PluginContext`.
@@ -0,0 +1,17 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "harmony-plugin-api"
7
+ version = "0.1.0"
8
+ description = "Pure plugin SDK for Harmony plugins"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ dependencies = []
12
+
13
+ [tool.setuptools]
14
+ package-dir = {"" = "src"}
15
+
16
+ [tool.setuptools.packages.find]
17
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,48 @@
1
+ from .context import (
2
+ PluginContext,
3
+ PluginDialogBridge,
4
+ PluginMediaBridge,
5
+ PluginServiceBridge,
6
+ PluginSettingsBridge,
7
+ PluginStorageBridge,
8
+ PluginThemeBridge,
9
+ PluginUiBridge,
10
+ )
11
+ from .cover import (
12
+ PluginArtistCoverResult,
13
+ PluginArtistCoverSource,
14
+ PluginCoverResult,
15
+ PluginCoverSource,
16
+ )
17
+ from .lyrics import PluginLyricsResult, PluginLyricsSource
18
+ from .manifest import Capability, PluginManifest, PluginManifestError
19
+ from .media import PluginPlaybackRequest, PluginTrack
20
+ from .online import PluginOnlineProvider
21
+ from .plugin import HarmonyPlugin
22
+ from .registry_types import SettingsTabSpec, SidebarEntrySpec
23
+
24
+ __all__ = [
25
+ "Capability",
26
+ "HarmonyPlugin",
27
+ "PluginArtistCoverResult",
28
+ "PluginArtistCoverSource",
29
+ "PluginContext",
30
+ "PluginCoverResult",
31
+ "PluginCoverSource",
32
+ "PluginDialogBridge",
33
+ "PluginLyricsResult",
34
+ "PluginLyricsSource",
35
+ "PluginManifest",
36
+ "PluginManifestError",
37
+ "PluginMediaBridge",
38
+ "PluginOnlineProvider",
39
+ "PluginPlaybackRequest",
40
+ "PluginServiceBridge",
41
+ "PluginSettingsBridge",
42
+ "PluginStorageBridge",
43
+ "PluginThemeBridge",
44
+ "PluginTrack",
45
+ "PluginUiBridge",
46
+ "SettingsTabSpec",
47
+ "SidebarEntrySpec",
48
+ ]
@@ -0,0 +1,126 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Any, Protocol
6
+
7
+ from .cover import PluginArtistCoverSource, PluginCoverSource
8
+ from .lyrics import PluginLyricsSource
9
+ from .manifest import PluginManifest
10
+ from .online import PluginOnlineProvider
11
+ from .registry_types import SettingsTabSpec, SidebarEntrySpec
12
+
13
+
14
+ class PluginSettingsBridge(Protocol):
15
+ def get(self, key: str, default: Any = None) -> Any:
16
+ ...
17
+
18
+ def set(self, key: str, value: Any) -> None:
19
+ ...
20
+
21
+
22
+ class PluginStorageBridge(Protocol):
23
+ @property
24
+ def data_dir(self) -> Path:
25
+ ...
26
+
27
+ @property
28
+ def cache_dir(self) -> Path:
29
+ ...
30
+
31
+ @property
32
+ def temp_dir(self) -> Path:
33
+ ...
34
+
35
+
36
+ class PluginThemeBridge(Protocol):
37
+ def register_widget(self, widget) -> None:
38
+ ...
39
+
40
+ def get_qss(self, template: str) -> str:
41
+ ...
42
+
43
+ def current_theme(self):
44
+ ...
45
+
46
+
47
+ class PluginDialogBridge(Protocol):
48
+ def information(self, parent, title: str, message: str):
49
+ ...
50
+
51
+ def warning(self, parent, title: str, message: str):
52
+ ...
53
+
54
+ def question(self, parent, title: str, message: str, buttons, default_button):
55
+ ...
56
+
57
+ def critical(self, parent, title: str, message: str):
58
+ ...
59
+
60
+ def setup_title_bar(self, dialog, container_layout, title: str, **kwargs):
61
+ ...
62
+
63
+
64
+ class PluginUiBridge(Protocol):
65
+ def register_sidebar_entry(self, spec: SidebarEntrySpec) -> None:
66
+ ...
67
+
68
+ def register_settings_tab(self, spec: SettingsTabSpec) -> None:
69
+ ...
70
+
71
+ @property
72
+ def theme(self) -> PluginThemeBridge:
73
+ ...
74
+
75
+ @property
76
+ def dialogs(self) -> PluginDialogBridge:
77
+ ...
78
+
79
+
80
+ class PluginMediaBridge(Protocol):
81
+ def cache_remote_track(self, request: Any, progress_callback=None, force: bool = False):
82
+ ...
83
+
84
+ def add_online_track(self, request: Any):
85
+ ...
86
+
87
+ def play_online_track(self, request: Any) -> int | None:
88
+ ...
89
+
90
+ def add_online_track_to_queue(self, request: Any) -> int | None:
91
+ ...
92
+
93
+ def insert_online_track_to_queue(self, request: Any) -> int | None:
94
+ ...
95
+
96
+
97
+ class PluginServiceBridge(Protocol):
98
+ def register_lyrics_source(self, source: PluginLyricsSource) -> None:
99
+ ...
100
+
101
+ def register_cover_source(self, source: PluginCoverSource) -> None:
102
+ ...
103
+
104
+ def register_artist_cover_source(self, source: PluginArtistCoverSource) -> None:
105
+ ...
106
+
107
+ def register_online_music_provider(self, provider: PluginOnlineProvider) -> None:
108
+ ...
109
+
110
+ @property
111
+ def media(self) -> PluginMediaBridge:
112
+ ...
113
+
114
+
115
+ @dataclass(frozen=True)
116
+ class PluginContext:
117
+ plugin_id: str
118
+ manifest: PluginManifest
119
+ logger: Any
120
+ http: Any
121
+ events: Any
122
+ language: str
123
+ storage: PluginStorageBridge
124
+ settings: PluginSettingsBridge
125
+ ui: PluginUiBridge
126
+ services: PluginServiceBridge
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Protocol
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class PluginCoverResult:
9
+ item_id: str
10
+ title: str
11
+ artist: str
12
+ album: str = ""
13
+ duration: float | None = None
14
+ source: str = ""
15
+ cover_url: str | None = None
16
+ extra_id: str | None = None
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class PluginArtistCoverResult:
21
+ artist_id: str
22
+ name: str
23
+ source: str = ""
24
+ cover_url: str | None = None
25
+ album_count: int | None = None
26
+
27
+
28
+ class PluginCoverSource(Protocol):
29
+ source_id: str
30
+ display_name: str
31
+
32
+ def search(
33
+ self,
34
+ title: str,
35
+ artist: str,
36
+ album: str = "",
37
+ duration: float | None = None,
38
+ ) -> list[PluginCoverResult]:
39
+ ...
40
+
41
+
42
+ class PluginArtistCoverSource(Protocol):
43
+ source_id: str
44
+ display_name: str
45
+
46
+ def search(
47
+ self,
48
+ artist_name: str,
49
+ limit: int = 10,
50
+ ) -> list[PluginArtistCoverResult]:
51
+ ...
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Protocol
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class PluginLyricsResult:
9
+ song_id: str
10
+ title: str
11
+ artist: str
12
+ album: str = ""
13
+ duration: float | None = None
14
+ source: str = ""
15
+ cover_url: str | None = None
16
+ lyrics: str | None = None
17
+ accesskey: str | None = None
18
+ supports_yrc: bool = False
19
+
20
+
21
+ class PluginLyricsSource(Protocol):
22
+ source_id: str
23
+ display_name: str
24
+
25
+ def search(
26
+ self,
27
+ title: str,
28
+ artist: str,
29
+ limit: int = 10,
30
+ ) -> list[PluginLyricsResult]:
31
+ ...
32
+
33
+ def get_lyrics(self, result: PluginLyricsResult) -> str | None:
34
+ ...
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Literal
5
+
6
+
7
+ Capability = Literal[
8
+ "sidebar",
9
+ "settings_tab",
10
+ "lyrics_source",
11
+ "cover",
12
+ "online_music_provider",
13
+ ]
14
+
15
+ _ALLOWED_CAPABILITIES = {
16
+ "sidebar",
17
+ "settings_tab",
18
+ "lyrics_source",
19
+ "cover",
20
+ "online_music_provider",
21
+ }
22
+
23
+
24
+ class PluginManifestError(ValueError):
25
+ pass
26
+
27
+
28
+ def _require_str(data: dict[str, Any], key: str) -> str:
29
+ value = data.get(key)
30
+ if not isinstance(value, str):
31
+ raise PluginManifestError(f"Manifest field '{key}' must be a string")
32
+ if not value.strip():
33
+ raise PluginManifestError(
34
+ f"Manifest field '{key}' must be a non-empty string"
35
+ )
36
+ return value
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class PluginManifest:
41
+ id: str
42
+ name: str
43
+ version: str
44
+ api_version: str
45
+ entrypoint: str
46
+ entry_class: str
47
+ capabilities: tuple[str, ...]
48
+ min_app_version: str
49
+ max_app_version: str | None = None
50
+
51
+ @classmethod
52
+ def from_dict(cls, data: dict[str, Any]) -> "PluginManifest":
53
+ required = (
54
+ "id",
55
+ "name",
56
+ "version",
57
+ "api_version",
58
+ "entrypoint",
59
+ "entry_class",
60
+ "capabilities",
61
+ "min_app_version",
62
+ )
63
+ missing = [key for key in required if key not in data]
64
+ if missing:
65
+ raise PluginManifestError(f"Missing manifest keys: {', '.join(missing)}")
66
+
67
+ capabilities_raw = data["capabilities"]
68
+ if isinstance(capabilities_raw, str) or not isinstance(
69
+ capabilities_raw, (list, tuple)
70
+ ):
71
+ raise PluginManifestError(
72
+ "Manifest field 'capabilities' must be a list/tuple of strings"
73
+ )
74
+ if not all(isinstance(item, str) for item in capabilities_raw):
75
+ raise PluginManifestError(
76
+ "Manifest field 'capabilities' must be a list/tuple of strings"
77
+ )
78
+ capabilities = tuple(capabilities_raw)
79
+ unknown = sorted(set(capabilities) - _ALLOWED_CAPABILITIES)
80
+ if unknown:
81
+ raise PluginManifestError(f"Unknown capabilities: {', '.join(unknown)}")
82
+
83
+ max_app_version = data.get("max_app_version")
84
+ if max_app_version is not None and not isinstance(max_app_version, str):
85
+ raise PluginManifestError(
86
+ "Manifest field 'max_app_version' must be a string if provided"
87
+ )
88
+
89
+ return cls(
90
+ id=_require_str(data, "id"),
91
+ name=_require_str(data, "name"),
92
+ version=_require_str(data, "version"),
93
+ api_version=_require_str(data, "api_version"),
94
+ entrypoint=_require_str(data, "entrypoint"),
95
+ entry_class=_require_str(data, "entry_class"),
96
+ capabilities=capabilities,
97
+ min_app_version=_require_str(data, "min_app_version"),
98
+ max_app_version=max_app_version,
99
+ )
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class PluginTrack:
9
+ track_id: str
10
+ title: str
11
+ artist: str
12
+ album: str = ""
13
+ duration: int | None = None
14
+ artwork_url: str | None = None
15
+ metadata: dict[str, Any] = field(default_factory=dict)
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class PluginPlaybackRequest:
20
+ provider_id: str
21
+ track_id: str
22
+ title: str
23
+ quality: str
24
+ metadata: dict[str, Any]
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Protocol
4
+
5
+ from .media import PluginPlaybackRequest, PluginTrack
6
+
7
+ __all__ = ["PluginOnlineProvider", "PluginPlaybackRequest", "PluginTrack"]
8
+
9
+
10
+ class PluginOnlineProvider(Protocol):
11
+ provider_id: str
12
+ display_name: str
13
+
14
+ def create_page(self, context: Any, parent: Any = None) -> Any:
15
+ ...
16
+
17
+ def get_playback_url_info(
18
+ self,
19
+ track_id: str,
20
+ quality: str,
21
+ ) -> dict[str, Any] | None:
22
+ ...
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Protocol
4
+
5
+ from .context import PluginContext
6
+
7
+
8
+ class HarmonyPlugin(Protocol):
9
+ plugin_id: str
10
+
11
+ def register(self, context: PluginContext) -> None:
12
+ ...
13
+
14
+ def unregister(self, context: PluginContext) -> None:
15
+ ...
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Callable
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class SidebarEntrySpec:
9
+ plugin_id: str
10
+ entry_id: str
11
+ title: str
12
+ order: int
13
+ icon_name: str | None
14
+ page_factory: Callable[[Any, Any], Any]
15
+ icon_path: str | None = None
16
+ title_provider: Callable[[], str] | None = None
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class SettingsTabSpec:
21
+ plugin_id: str
22
+ tab_id: str
23
+ title: str
24
+ order: int
25
+ widget_factory: Callable[[Any, Any], Any]
26
+ title_provider: Callable[[], str] | None = None
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: harmony-plugin-api
3
+ Version: 0.1.0
4
+ Summary: Pure plugin SDK for Harmony plugins
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+
8
+ # harmony-plugin-api
9
+
10
+ Pure plugin SDK for Harmony plugins.
11
+
12
+ This package only contains stable plugin-facing protocols, models, manifest parsing,
13
+ and registry spec types. It does not include Harmony host runtime implementations.
14
+
15
+ Host-specific theme, dialog, runtime, and bootstrap integrations must be provided by
16
+ the Harmony application and injected through `PluginContext`.
@@ -0,0 +1,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/harmony_plugin_api/__init__.py
4
+ src/harmony_plugin_api/context.py
5
+ src/harmony_plugin_api/cover.py
6
+ src/harmony_plugin_api/lyrics.py
7
+ src/harmony_plugin_api/manifest.py
8
+ src/harmony_plugin_api/media.py
9
+ src/harmony_plugin_api/online.py
10
+ src/harmony_plugin_api/plugin.py
11
+ src/harmony_plugin_api/registry_types.py
12
+ src/harmony_plugin_api.egg-info/PKG-INFO
13
+ src/harmony_plugin_api.egg-info/SOURCES.txt
14
+ src/harmony_plugin_api.egg-info/dependency_links.txt
15
+ src/harmony_plugin_api.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ harmony_plugin_api