feat(lib): add editing functionality, add logging, add categories class
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"]
|
||||
|
||||
159
src/logic_plugin_manager/tags/category.py
Normal file
159
src/logic_plugin_manager/tags/category.py
Normal 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"]
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user