harmony-plugin-api 0.1.0__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.
- harmony_plugin_api/__init__.py +48 -0
- harmony_plugin_api/context.py +126 -0
- harmony_plugin_api/cover.py +51 -0
- harmony_plugin_api/lyrics.py +34 -0
- harmony_plugin_api/manifest.py +99 -0
- harmony_plugin_api/media.py +24 -0
- harmony_plugin_api/online.py +22 -0
- harmony_plugin_api/plugin.py +15 -0
- harmony_plugin_api/registry_types.py +26 -0
- harmony_plugin_api-0.1.0.dist-info/METADATA +16 -0
- harmony_plugin_api-0.1.0.dist-info/RECORD +13 -0
- harmony_plugin_api-0.1.0.dist-info/WHEEL +5 -0
- harmony_plugin_api-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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,13 @@
|
|
|
1
|
+
harmony_plugin_api/__init__.py,sha256=iDJRmODGObV7TjkObODdxn0sRFUQtgj-TQmMwCNomfE,1248
|
|
2
|
+
harmony_plugin_api/context.py,sha256=xwrvJdO2cVkvMI4TGM28a178WRZ6Yia0iHD9tqVoJlU,2933
|
|
3
|
+
harmony_plugin_api/cover.py,sha256=bZriYoqrtGvZ-qzYNtEROZ-oM9SpEL7zDb9LyIaBgHU,994
|
|
4
|
+
harmony_plugin_api/lyrics.py,sha256=J80kfjH30kCQu6VKWU8Xyjb9hubkE-yAaE1nu3lfCrU,704
|
|
5
|
+
harmony_plugin_api/manifest.py,sha256=HkLMLpwWLqPz-wt7gXums0-KiXUhBoINTvFJKaYJfwA,2978
|
|
6
|
+
harmony_plugin_api/media.py,sha256=q6EitBdC6Kok-RFNiL3HGTT5014u0TKu6MbQqULGJcQ,495
|
|
7
|
+
harmony_plugin_api/online.py,sha256=YdNpYqDgJ1-ioM8alT8OUjXj0X3RAEBtnOztJi0YjUk,499
|
|
8
|
+
harmony_plugin_api/plugin.py,sha256=gb_xJxsbiVe92w9l1Ha_Vxxq4cho4iIqSfvmdDGZM-M,292
|
|
9
|
+
harmony_plugin_api/registry_types.py,sha256=865dUJL0N4JLT2WtOx24Tq-0XeDhzLNguzOWtXJNRX0,587
|
|
10
|
+
harmony_plugin_api-0.1.0.dist-info/METADATA,sha256=N3t9OewjiACRTuZ4qPH2Yux-_lWIpNbZosZFvIv3JzU,548
|
|
11
|
+
harmony_plugin_api-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
12
|
+
harmony_plugin_api-0.1.0.dist-info/top_level.txt,sha256=rcwqJQgD3vPenHOfVtcdrv0Lpfkm3jpJuYl4nP7gtgk,19
|
|
13
|
+
harmony_plugin_api-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
harmony_plugin_api
|