feat(lib): add editing functionality, add logging, add categories class

This commit is contained in:
h
2025-11-07 16:00:41 +01:00
parent 9e2544e506
commit ba3005d9b4
10 changed files with 692 additions and 32 deletions

View File

@@ -1,11 +1,16 @@
import logging
from .components import AudioComponent, AudioUnitType, Component
from .exceptions import MusicAppsLoadError, PluginLoadError, TagsetLoadError
from .logic import Logic, Plugins, SearchResult
from .tags import MusicApps, Properties, Tagpool, Tagset
from .tags import Category, MusicApps, Properties, Tagpool, Tagset
logging.getLogger(__name__).addHandler(logging.NullHandler())
__all__ = [
"AudioComponent",
"AudioUnitType",
"Category",
"Component",
"Logic",
"MusicApps",

View File

@@ -1,10 +1,13 @@
from dataclasses import dataclass
import logging
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from .. import defaults
from ..exceptions import CannotParseComponentError
from ..tags import Tagset
from ..tags import Category, MusicApps, Tagset
logger = logging.getLogger(__name__)
class AudioUnitType(Enum):
@@ -62,12 +65,19 @@ class AudioComponent:
version: int
tags_id: str
tagset: Tagset
categories: list[Category] = field(default_factory=list)
def __init__(
self, data: dict, *, lazy: bool = False, tags_path: Path = defaults.tags_path
self,
data: dict,
*,
lazy: bool = False,
tags_path: Path = defaults.tags_path,
musicapps: MusicApps = None,
):
self.tags_path = tags_path
self.lazy = lazy
self.musicapps = musicapps or MusicApps(tags_path=self.tags_path, lazy=lazy)
try:
self.full_name = data.get("name")
@@ -85,17 +95,34 @@ class AudioComponent:
f"{self.subtype_code.encode('ascii').hex()}-"
f"{self.manufacturer_code.encode('ascii').hex()}"
)
logger.debug(f"Created AudioComponent {self.full_name} from data")
except Exception as e:
raise CannotParseComponentError(f"An error occurred while parsing: {e}")
raise CannotParseComponentError(
f"An error occurred while parsing: {e}"
) from e
if not lazy:
self.load()
def load(self) -> "AudioComponent":
logger.debug(f"Loading AudioComponent {self.full_name}")
self.tagset = Tagset(self.tags_path / self.tags_id, lazy=self.lazy)
logger.debug(f"Loaded Tagset for {self.full_name}")
self.categories = []
for name in self.tagset.tags.keys():
try:
logger.debug(f"Loading category {name} for {self.full_name}")
self.categories.append(
Category(name, musicapps=self.musicapps, lazy=self.lazy)
)
except Exception as e:
logger.warning(
f"Failed to load category {name} for {self.full_name}: {e}"
)
logger.debug(f"Loaded {len(self.categories)} categories for {self.full_name}")
return self
def __eq__(self, other):
def __eq__(self, other) -> bool:
if not isinstance(other, AudioComponent):
return NotImplemented
return self.tags_id == other.tags_id
@@ -103,5 +130,42 @@ class AudioComponent:
def __hash__(self):
return hash(self.tags_id)
def set_nickname(self, nickname: str) -> "AudioComponent":
self.tagset.set_nickname(nickname)
self.load()
return self
def set_shortname(self, shortname: str) -> "AudioComponent":
self.tagset.set_shortname(shortname)
self.load()
return self
def set_categories(self, categories: list[Category]) -> "AudioComponent":
self.tagset.set_tags({category.name: "user" for category in categories})
self.load()
return self
def add_to_category(self, category: Category) -> "AudioComponent":
self.tagset.add_tag(category.name, "user")
self.load()
return self
def remove_from_category(self, category: Category) -> "AudioComponent":
self.tagset.remove_tag(category.name)
self.load()
return self
def move_to_category(self, category: Category) -> "AudioComponent":
self.tagset.move_to_tag(category.name, "user")
self.load()
return self
def move_to_parents(self) -> "AudioComponent":
for category in self.categories:
self.tagset.add_tag(category.parent.name, "user")
self.tagset.remove_tag(category.name)
self.load()
return self
__all__ = ["AudioComponent", "AudioUnitType"]

View File

@@ -1,3 +1,4 @@
import logging
import plistlib
from dataclasses import dataclass
from pathlib import Path
@@ -9,8 +10,11 @@ from ..exceptions import (
NonexistentPlistError,
OldComponentFormatError,
)
from ..tags import MusicApps
from .audiocomponent import AudioComponent
logger = logging.getLogger(__name__)
@dataclass
class Component:
@@ -21,16 +25,25 @@ class Component:
audio_components: list[AudioComponent]
def __init__(
self, path: Path, *, lazy: bool = False, tags_path: Path = defaults.tags_path
self,
path: Path,
*,
lazy: bool = False,
tags_path: Path = defaults.tags_path,
musicapps: MusicApps = None,
):
self.path = path if path.suffix == ".component" else Path(f"{path}.component")
self.lazy = lazy
self.tags_path = tags_path
self.musicapps = musicapps or MusicApps(tags_path=self.tags_path, lazy=lazy)
logger.debug(f"Created Component from {self.path}")
if not lazy:
self.load()
def _parse_plist(self):
info_plist_path = self.path / "Contents" / "Info.plist"
logger.debug(f"Parsing Info.plist at {info_plist_path}")
if not info_plist_path.exists():
raise NonexistentPlistError(f"Info.plist not found at {info_plist_path}")
@@ -39,25 +52,36 @@ class Component:
plist_data = plistlib.load(fp)
return plist_data
except Exception as e:
raise CannotParsePlistError(f"An error occurred: {e}")
raise CannotParsePlistError(f"An error occurred: {e}") from e
def load(self) -> "Component":
plist_data = self._parse_plist()
logger.debug(f"Loaded Info.plist for {self.path}")
try:
self.name = self.path.name.removesuffix(".component")
self.bundle_id = plist_data["CFBundleIdentifier"]
self.version = plist_data["CFBundleVersion"]
self.short_version = plist_data["CFBundleShortVersionString"]
self.bundle_id = plist_data.get("CFBundleIdentifier")
self.version = plist_data.get("CFBundleVersion")
self.short_version = plist_data.get("CFBundleShortVersionString")
logger.debug(f"Loaded component info for {self.bundle_id}")
except Exception as e:
raise CannotParsePlistError(
f"An error occurred while extracting: {e}"
) from e
try:
logger.debug(f"Loading components for {self.bundle_id}")
self.audio_components = [
AudioComponent(name, lazy=self.lazy, tags_path=self.tags_path)
AudioComponent(
name,
lazy=self.lazy,
tags_path=self.tags_path,
musicapps=self.musicapps,
)
for name in plist_data["AudioComponents"]
]
logger.debug(
f"Loaded {len(self.audio_components)} components for {self.bundle_id}"
)
except KeyError as e:
raise OldComponentFormatError(
"This component is in an old format and cannot be loaded"
@@ -67,6 +91,7 @@ class Component:
"An error occurred while loading components"
) from e
logger.debug(f"Loaded {self.name} from {self.path}")
return self
def __hash__(self):

View File

@@ -30,5 +30,21 @@ class CannotParseTagsetError(TagsetLoadError):
pass
class TagsetWriteError(TagsetLoadError):
pass
class MusicAppsLoadError(Exception):
pass
class MusicAppsWriteError(Exception):
pass
class CategoryValidationError(Exception):
pass
class CategoryExistsError(Exception):
pass

View File

@@ -1,17 +1,21 @@
import logging
from dataclasses import dataclass
from pathlib import Path
from .. import defaults
from ..components import Component
from ..tags import MusicApps
from ..components import AudioComponent, Component
from ..tags import Category, MusicApps
from .plugins import Plugins
logger = logging.getLogger(__name__)
@dataclass
class Logic:
musicapps: MusicApps
plugins: Plugins
components: set[Component]
categories: dict[str, Category]
components_path: Path = defaults.components_path
tags_path: Path = defaults.tags_path
@@ -33,22 +37,92 @@ class Logic:
self.musicapps = MusicApps(tags_path=self.tags_path, lazy=lazy)
self.plugins = Plugins()
self.components = set()
self.categories = {}
self.lazy = lazy
logger.debug("Created Logic instance")
if not lazy:
self.discover_plugins()
self.discover_categories()
def discover_plugins(self):
def discover_plugins(self) -> "Logic":
for component_path in self.components_path.glob("*.component"):
try:
component = Component(component_path, lazy=self.lazy)
logger.debug(f"Loading component {component_path}")
component = Component(
component_path, lazy=self.lazy, musicapps=self.musicapps
)
self.components.add(component)
logger.debug(f"Loading plugins for {component.name}")
for plugin in component.audio_components:
self.plugins.add(plugin, lazy=self.lazy)
except Exception as e:
assert e
continue
logger.warning(f"Failed to load component {component_path}: {e}")
return self
def discover_categories(self) -> "Logic":
for category in self.musicapps.tagpool.categories.keys():
logger.debug(f"Loading category {category}")
self.categories[category] = Category(
category, musicapps=self.musicapps, lazy=self.lazy
)
return self
def sync_category_plugin_amount(self, category: Category | str) -> "Logic":
if isinstance(category, str):
category = self.categories[category]
logger.debug(f"Syncing plugin amount for {category.name}")
category.update_plugin_amount(
len(
self.plugins.get_by_category(
category.name if isinstance(category, Category) else category
)
)
)
return self
def sync_all_categories_plugin_amount(self) -> "Logic":
for category in self.categories.values():
self.sync_category_plugin_amount(category)
return self
def search_categories(self, query: str) -> set[Category]:
return {
category
for category in self.categories.values()
if query in category.name.lower()
}
def introduce_category(self, name: str) -> Category:
return Category.introduce(name, musicapps=self.musicapps, lazy=self.lazy)
def add_plugins_to_category(
self, category: Category, plugins: set[AudioComponent]
) -> "Logic":
for plugin in plugins:
plugin.add_to_category(category)
self.sync_category_plugin_amount(category)
return self
def move_plugins_to_category(
self, category: Category, plugins: set[AudioComponent]
) -> "Logic":
for plugin in plugins:
plugin.move_to_category(category)
self.sync_category_plugin_amount(category)
return self
def remove_plugins_from_category(
self, category: Category, plugins: set[AudioComponent]
) -> "Logic":
for plugin in plugins:
plugin.remove_from_category(category)
self.sync_category_plugin_amount(category)
return self
__all__ = ["Logic"]

View File

@@ -1,8 +1,11 @@
import logging
from collections import defaultdict
from dataclasses import dataclass
from ..components import AudioComponent, AudioUnitType
logger = logging.getLogger(__name__)
@dataclass
class SearchResult:
@@ -26,13 +29,16 @@ class Plugins:
self._by_category: dict[str | None, set[AudioComponent]] = defaultdict(set)
def add(self, plugin: AudioComponent, *, lazy: bool = False) -> "Plugins":
logger.debug(f"Adding plugin {plugin.full_name}")
self._plugins.add(plugin)
if not lazy:
self._index_plugin(plugin)
return self
def _index_plugin(self, plugin: AudioComponent):
logger.debug(f"Indexing plugin {plugin.full_name}")
if plugin.lazy:
logger.debug(f"{plugin.full_name} is lazy, loading first")
plugin.load()
plugin.tagset.load()
@@ -48,8 +54,10 @@ class Plugins:
self._by_category[tag.lower()].add(plugin)
if not plugin.tagset.tags.keys():
self._by_category[None].add(plugin)
logger.debug(f"Indexed plugin {plugin.full_name}")
def reindex_all(self):
logger.debug("Reindexing all plugins")
self._by_full_name.clear()
self._by_manufacturer.clear()
self._by_name.clear()
@@ -59,9 +67,11 @@ class Plugins:
self._by_subtype_code.clear()
self._by_tags_id.clear()
self._by_category.clear()
logger.debug("Cleared all indexes")
for plugin in self._plugins:
self._index_plugin(plugin)
logger.debug("Reindexed all plugins")
def all(self):
return self._plugins.copy()

View File

@@ -1,4 +1,5 @@
from .category import Category
from .musicapps import MusicApps, Properties, Tagpool
from .tagset import Tagset
__all__ = ["MusicApps", "Properties", "Tagpool", "Tagset"]
__all__ = ["Category", "MusicApps", "Properties", "Tagpool", "Tagset"]

View File

@@ -0,0 +1,159 @@
import logging
from dataclasses import dataclass, field
from ..exceptions import CategoryExistsError, CategoryValidationError
from .musicapps import MusicApps
logger = logging.getLogger(__name__)
@dataclass
class Category:
name: str
musicapps: MusicApps = field(repr=False)
is_root: bool
plugin_amount: int
lazy: bool
def __init__(self, name: str, *, musicapps: MusicApps = None, lazy: bool = False):
self.name = name
self.musicapps = musicapps or MusicApps(lazy=lazy)
self.is_root = False
self.plugin_amount = 0
self.lazy = lazy
if not lazy:
self.load()
def load(self):
logger.debug(f"Validating category {self.name}")
if self.name not in self.musicapps.tagpool.categories.keys():
raise CategoryValidationError(f"Category {self.name} not found in tagpool")
self.plugin_amount = self.musicapps.tagpool.categories[self.name]
logger.debug(f"Loaded plugin amount for {self.name} - {self.plugin_amount}")
if self.name == "":
self.is_root = True
logger.debug("This is the root category")
return
if self.name not in self.musicapps.properties.sorting:
raise CategoryValidationError(f"Category {self.name} not found in sorting")
logger.debug(f"Valid category {self.name}")
@classmethod
def introduce(cls, name: str, *, musicapps: MusicApps = None, lazy: bool = False):
logger.debug(f"Introducing category {name}")
if musicapps is None:
musicapps = MusicApps()
try:
cls(name, musicapps=musicapps, lazy=lazy)
raise CategoryExistsError(f"Category {name} already exists")
except CategoryValidationError:
logger.debug(f"Category {name} doesn't exist, proceeding")
pass
musicapps.introduce_category(name)
logger.debug(f"Introduced category {name}")
return cls(name, musicapps=musicapps)
@property
def parent(self) -> "Category":
if self.is_root:
return self
return self.__class__(
":".join(self.name.split(":")[:-1]),
musicapps=self.musicapps,
lazy=self.lazy,
)
def child(self, name: str) -> "Category":
return self.__class__(
f"{self.name}:{name}", musicapps=self.musicapps, lazy=self.lazy
)
def delete(self):
if self.is_root:
return
self.musicapps.tagpool.remove_category(self.name)
self.musicapps.properties.remove_category(self.name)
def update_plugin_amount(self, amount: int):
if self.is_root:
return
self.musicapps.tagpool.write_category(self.name, amount)
self.load()
def move_up(self, steps: int = 1):
if self.is_root:
return
self.musicapps.properties.move_up(self.name, steps)
self.load()
def move_down(self, steps: int = 1):
if self.is_root:
return
self.musicapps.properties.move_down(self.name, steps)
self.load()
def move_to_top(self):
if self.is_root:
return
self.musicapps.properties.move_to_top(self.name)
self.load()
def move_to_bottom(self):
if self.is_root:
return
self.musicapps.properties.move_to_bottom(self.name)
self.load()
def move_before(self, other: "Category"):
if self.is_root:
return
self.musicapps.properties.move_before(self.name, other.name)
self.load()
def move_after(self, other: "Category"):
if self.is_root:
return
self.musicapps.properties.move_after(self.name, other.name)
self.load()
def move_to(self, index: int):
if self.is_root:
return
self.musicapps.properties.move_to_index(self.name, index)
self.load()
def swap(self, other: "Category"):
if self.is_root:
return
self.musicapps.properties.swap(self.name, other.name)
self.load()
@property
def index(self):
return self.musicapps.properties.get_index(self.name)
@property
def neighbors(self):
if self.is_root:
return None, None
neighbors = self.musicapps.properties.get_neighbors(self.name)
if neighbors is None or len(neighbors) != 2:
return None, None
return (
self.__class__(name=neighbors[0], musicapps=self.musicapps),
self.__class__(name=neighbors[1], musicapps=self.musicapps),
)
@property
def is_first(self):
return self.musicapps.properties.is_first(self.name)
@property
def is_last(self):
return self.musicapps.properties.is_last(self.name)
__all__ = ["Category"]

View File

@@ -1,20 +1,35 @@
import logging
import plistlib
from dataclasses import dataclass
from dataclasses import dataclass, field
from pathlib import Path
from .. import defaults
from ..exceptions import MusicAppsLoadError
from ..exceptions import MusicAppsLoadError, MusicAppsWriteError
logger = logging.getLogger(__name__)
def _parse_plist(path: Path):
logger.debug(f"Parsing plist at {path}")
if not path.exists():
raise MusicAppsLoadError(f"File not found at {path}")
try:
with open(path, "rb") as fp:
plist_data = plistlib.load(fp)
logger.debug(f"Parsed plist for {path}")
return plist_data
except Exception as e:
raise MusicAppsLoadError(f"An error occurred: {e}")
raise MusicAppsLoadError(f"An error occurred: {e}") from e
def _save_plist(path: Path, data: dict):
logger.debug(f"Saving plist to {path}")
try:
with open(path, "wb") as fp:
plistlib.dump(data, fp)
logger.debug(f"Saved plist to {path}")
except Exception as e:
raise MusicAppsWriteError(f"An error occurred: {e}") from e
@dataclass
@@ -25,32 +40,263 @@ class Tagpool:
self.path = tags_path / "MusicApps.tagpool"
self.lazy = lazy
logger.debug(f"Created Tagpool from {self.path}")
if not lazy:
self.load()
def load(self) -> "Tagpool":
logger.debug(f"Loading Tagpool data from {self.path}")
self.categories = _parse_plist(self.path)
logger.debug(f"Loaded Tagpool data from {self.path}")
return self
def write_category(self, name: str, plugin_count: int = 0):
self.load()
self.categories[name] = plugin_count
_save_plist(self.path, self.categories)
self.load()
def introduce_category(self, name: str):
self.load()
if name in self.categories:
return
self.write_category(name)
def remove_category(self, name: str):
self.load()
self.categories.pop(name, None)
_save_plist(self.path, self.categories)
self.load()
@dataclass
class Properties:
sorting: list[str]
user_sorted: bool
__raw_data: dict[str, str | list[str] | bool] = field(repr=False)
def __init__(self, tags_path: Path, *, lazy: bool = False):
self.path = tags_path / "MusicApps.properties"
self.lazy = lazy
logger.debug(f"Created Properties from {self.path}")
if not lazy:
self.load()
def load(self) -> "Properties":
properties_data = _parse_plist(self.path)
self.sorting = properties_data.get("sorting", [])
self.user_sorted = bool(properties_data.get("user_sorted", False))
logger.debug(f"Loading Properties data from {self.path}")
self.__raw_data = _parse_plist(self.path)
logger.debug(f"Loaded Properties data from {self.path}")
self.sorting = self.__raw_data.get("sorting", [])
self.user_sorted = bool(self.__raw_data.get("user_sorted", False))
logger.debug(f"Parsed Properties data from {self.path}")
return self
def introduce_category(self, name: str):
self.load()
if name in self.sorting:
return
self.__raw_data["sorting"].append(name)
_save_plist(self.path, self.__raw_data)
self.load()
def enable_user_sorting(self):
self.load()
self.__raw_data["user_sorted"] = "property"
_save_plist(self.path, self.__raw_data)
self.load()
def enable_alphabetical_sorting(self):
self.load()
del self.__raw_data["user_sorted"]
_save_plist(self.path, self.__raw_data)
self.load()
def remove_category(self, name: str):
self.load()
self.__raw_data["sorting"].remove(name)
_save_plist(self.path, self.__raw_data)
self.load()
def move_up(self, category: str, steps: int = 1):
self.load()
sorting = self.sorting.copy()
if category not in sorting:
raise ValueError(f"Category '{category}' not found in sorting")
current_idx = sorting.index(category)
new_idx = max(0, current_idx - steps)
sorting.pop(current_idx)
sorting.insert(new_idx, category)
self.__raw_data["sorting"] = sorting
_save_plist(self.path, self.__raw_data)
self.load()
def move_down(self, category: str, steps: int = 1):
self.load()
sorting = self.sorting.copy()
if category not in sorting:
raise ValueError(f"Category '{category}' not found in sorting")
current_idx = sorting.index(category)
new_idx = min(len(sorting) - 1, current_idx + steps)
sorting.pop(current_idx)
sorting.insert(new_idx, category)
self.__raw_data["sorting"] = sorting
_save_plist(self.path, self.__raw_data)
self.load()
def move_to_top(self, category: str):
self.load()
sorting = self.sorting.copy()
if category not in sorting:
raise ValueError(f"Category '{category}' not found in sorting")
sorting.remove(category)
sorting.insert(0, category)
self.__raw_data["sorting"] = sorting
_save_plist(self.path, self.__raw_data)
self.load()
def move_to_bottom(self, category: str):
self.load()
sorting = self.sorting.copy()
if category not in sorting:
raise ValueError(f"Category '{category}' not found in sorting")
sorting.remove(category)
sorting.append(category)
self.__raw_data["sorting"] = sorting
_save_plist(self.path, self.__raw_data)
self.load()
def move_before(self, category: str, target: str):
self.load()
sorting = self.sorting.copy()
if category not in sorting:
raise ValueError(f"Category '{category}' not found")
if target not in sorting:
raise ValueError(f"Target category '{target}' not found")
sorting.remove(category)
target_idx = sorting.index(target)
sorting.insert(target_idx, category)
self.__raw_data["sorting"] = sorting
_save_plist(self.path, self.__raw_data)
self.load()
def move_after(self, category: str, target: str):
self.load()
sorting = self.sorting.copy()
if category not in sorting:
raise ValueError(f"Category '{category}' not found")
if target not in sorting:
raise ValueError(f"Target category '{target}' not found")
sorting.remove(category)
target_idx = sorting.index(target)
sorting.insert(target_idx + 1, category)
self.__raw_data["sorting"] = sorting
_save_plist(self.path, self.__raw_data)
self.load()
def move_to_index(self, category: str, index: int):
self.load()
sorting = self.sorting.copy()
if category not in sorting:
raise ValueError(f"Category '{category}' not found")
if index < 0:
index = len(sorting) + index
index = max(0, min(len(sorting) - 1, index))
sorting.remove(category)
sorting.insert(index, category)
self.__raw_data["sorting"] = sorting
_save_plist(self.path, self.__raw_data)
self.load()
def swap(self, category1: str, category2: str):
self.load()
sorting = self.sorting.copy()
if category1 not in sorting or category2 not in sorting:
raise ValueError("Both categories must exist")
idx1 = sorting.index(category1)
idx2 = sorting.index(category2)
sorting[idx1], sorting[idx2] = sorting[idx2], sorting[idx1]
self.__raw_data["sorting"] = sorting
_save_plist(self.path, self.__raw_data)
self.load()
def set_order(self, categories: list[str]):
self.load()
current = set(self.sorting)
new = set(categories)
if new != current:
missing = current - new
extra = new - current
raise ValueError(f"Category mismatch. Missing: {missing}, Extra: {extra}")
self.__raw_data["sorting"] = categories
_save_plist(self.path, self.__raw_data)
self.load()
def reorder(self, key_func=None, reverse: bool = False):
self.load()
sorting = self.sorting.copy()
if key_func is None:
sorting.sort(reverse=reverse)
else:
sorting.sort(key=key_func, reverse=reverse)
self.__raw_data["sorting"] = sorting
_save_plist(self.path, self.__raw_data)
self.load()
def get_index(self, category: str) -> int:
return self.sorting.index(category)
def get_at_index(self, index: int) -> str:
return self.sorting[index]
def get_neighbors(self, category: str) -> tuple[str | None, str | None]:
idx = self.get_index(category)
prev_cat = self.sorting[idx - 1] if idx > 0 else None
next_cat = self.sorting[idx + 1] if idx < len(self.sorting) - 1 else None
return prev_cat, next_cat
def is_first(self, category: str) -> bool:
return self.get_index(category) == 0
def is_last(self, category: str) -> bool:
return self.get_index(category) == len(self.sorting) - 1
@dataclass
class MusicApps:
@@ -61,13 +307,24 @@ class MusicApps:
self.path = tags_path
self.lazy = lazy
logger.debug(f"Created MusicApps from {self.path}")
if not lazy:
self.load()
def load(self) -> "MusicApps":
self.tagpool = Tagpool(self.path, lazy=self.lazy)
self.properties = Properties(self.path, lazy=self.lazy)
logger.debug(f"Loaded MusicApps from {self.path}")
return self
def introduce_category(self, name: str):
self.tagpool.introduce_category(name)
self.properties.introduce_category(name)
def remove_category(self, name: str):
self.tagpool.remove_category(name)
self.properties.remove_category(name)
__all__ = ["MusicApps", "Properties", "Tagpool"]

View File

@@ -1,8 +1,12 @@
import plistlib
from dataclasses import dataclass
from dataclasses import dataclass, field
from pathlib import Path
from ..exceptions import CannotParseTagsetError, NonexistentTagsetError
from ..exceptions import (
CannotParseTagsetError,
NonexistentTagsetError,
TagsetWriteError,
)
@dataclass
@@ -11,6 +15,7 @@ class Tagset:
nickname: str
shortname: str
tags: dict[str, str]
__raw_data: dict[str, str | dict[str, str]] = field(repr=False)
def __init__(self, path: Path, *, lazy: bool = False):
self.path = path.with_suffix(".tagset")
@@ -27,17 +32,61 @@ class Tagset:
plist_data = plistlib.load(fp)
return plist_data
except Exception as e:
raise CannotParseTagsetError(f"An error occurred: {e}")
raise CannotParseTagsetError(f"An error occurred: {e}") from e
def _write_plist(self):
try:
with open(self.path, "wb") as fp:
plistlib.dump(self.__raw_data, fp)
except Exception as e:
raise TagsetWriteError(f"An error occurred: {e}") from e
def load(self) -> "Tagset":
tagset_data = self._parse_plist()
self.__raw_data = self._parse_plist()
self.tags_id = self.path.name.removesuffix(".tagset")
self.nickname = tagset_data.get("nickname")
self.shortname = tagset_data.get("shortname")
self.tags = tagset_data.get("tags") or {}
self.nickname = self.__raw_data.get("nickname")
self.shortname = self.__raw_data.get("shortname")
self.tags = self.__raw_data.get("tags") or {}
return self
def set_nickname(self, nickname: str):
self.load()
self.__raw_data["nickname"] = nickname
self._write_plist()
self.load()
def set_shortname(self, shortname: str):
self.load()
self.__raw_data["shortname"] = shortname
self._write_plist()
self.load()
def set_tags(self, tags: dict[str, str]):
self.load()
self.__raw_data["tags"] = tags
self._write_plist()
self.load()
def add_tag(self, tag: str, value: str):
self.load()
self.tags[tag] = value
self._write_plist()
self.load()
def remove_tag(self, tag: str):
self.load()
del self.tags[tag]
self._write_plist()
self.load()
def move_to_tag(self, tag: str, value: str):
self.load()
self.tags.clear()
self.tags[tag] = value
self._write_plist()
self.load()
__all__ = ["Tagset"]