2023-05-21 12:15:44 +00:00
|
|
|
from abc import ABC, abstractmethod
|
2023-05-21 10:24:59 +00:00
|
|
|
from logging import Logger
|
2023-05-22 05:48:12 +00:00
|
|
|
from typing import Optional, TYPE_CHECKING, Union
|
2023-05-17 09:13:53 +00:00
|
|
|
from PIL.Image import Image as PILImageType
|
2023-05-21 12:15:44 +00:00
|
|
|
|
2023-05-23 08:59:43 +00:00
|
|
|
from invokeai.app.models.image import (
|
|
|
|
ImageCategory,
|
|
|
|
ImageType,
|
|
|
|
InvalidImageCategoryException,
|
|
|
|
InvalidImageTypeException,
|
|
|
|
)
|
2023-05-21 12:15:44 +00:00
|
|
|
from invokeai.app.models.metadata import ImageMetadata
|
2023-05-21 10:05:33 +00:00
|
|
|
from invokeai.app.services.image_record_storage import (
|
2023-05-23 08:59:43 +00:00
|
|
|
ImageRecordDeleteException,
|
|
|
|
ImageRecordNotFoundException,
|
|
|
|
ImageRecordSaveException,
|
2023-05-21 10:05:33 +00:00
|
|
|
ImageRecordStorageBase,
|
2023-05-17 09:13:53 +00:00
|
|
|
)
|
2023-05-21 10:05:33 +00:00
|
|
|
from invokeai.app.services.models.image_record import (
|
|
|
|
ImageRecord,
|
|
|
|
ImageDTO,
|
2023-05-25 13:47:18 +00:00
|
|
|
ImageRecordChanges,
|
2023-05-21 10:05:33 +00:00
|
|
|
image_record_to_dto,
|
|
|
|
)
|
2023-05-23 08:59:43 +00:00
|
|
|
from invokeai.app.services.image_file_storage import (
|
|
|
|
ImageFileDeleteException,
|
|
|
|
ImageFileNotFoundException,
|
|
|
|
ImageFileSaveException,
|
|
|
|
ImageFileStorageBase,
|
|
|
|
)
|
2023-05-22 05:48:12 +00:00
|
|
|
from invokeai.app.services.item_storage import ItemStorageABC, PaginatedResults
|
2023-05-17 09:13:53 +00:00
|
|
|
from invokeai.app.services.metadata import MetadataServiceBase
|
2023-05-26 23:10:02 +00:00
|
|
|
from invokeai.app.services.resource_name import NameServiceBase
|
2023-05-17 09:13:53 +00:00
|
|
|
from invokeai.app.services.urls import UrlServiceBase
|
|
|
|
|
2023-05-22 05:48:12 +00:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
from invokeai.app.services.graph import GraphExecutionState
|
|
|
|
|
|
|
|
|
2023-05-21 12:15:44 +00:00
|
|
|
class ImageServiceABC(ABC):
|
2023-05-22 09:44:35 +00:00
|
|
|
"""High-level service for image management."""
|
2023-05-21 12:15:44 +00:00
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
def create(
|
|
|
|
self,
|
|
|
|
image: PILImageType,
|
|
|
|
image_type: ImageType,
|
|
|
|
image_category: ImageCategory,
|
|
|
|
node_id: Optional[str] = None,
|
|
|
|
session_id: Optional[str] = None,
|
2023-05-25 13:47:18 +00:00
|
|
|
intermediate: bool = False,
|
2023-05-27 08:32:16 +00:00
|
|
|
show_in_gallery: bool = True,
|
2023-05-21 12:15:44 +00:00
|
|
|
) -> ImageDTO:
|
|
|
|
"""Creates an image, storing the file and its metadata."""
|
|
|
|
pass
|
|
|
|
|
2023-05-25 13:47:18 +00:00
|
|
|
@abstractmethod
|
|
|
|
def update(
|
|
|
|
self,
|
|
|
|
image_type: ImageType,
|
|
|
|
image_name: str,
|
|
|
|
changes: ImageRecordChanges,
|
|
|
|
) -> ImageDTO:
|
|
|
|
"""Updates an image."""
|
|
|
|
pass
|
|
|
|
|
2023-05-21 12:15:44 +00:00
|
|
|
@abstractmethod
|
|
|
|
def get_pil_image(self, image_type: ImageType, image_name: str) -> PILImageType:
|
|
|
|
"""Gets an image as a PIL image."""
|
|
|
|
pass
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
def get_record(self, image_type: ImageType, image_name: str) -> ImageRecord:
|
|
|
|
"""Gets an image record."""
|
|
|
|
pass
|
|
|
|
|
2023-05-23 08:59:43 +00:00
|
|
|
@abstractmethod
|
|
|
|
def get_dto(self, image_type: ImageType, image_name: str) -> ImageDTO:
|
|
|
|
"""Gets an image DTO."""
|
|
|
|
pass
|
|
|
|
|
2023-05-21 12:15:44 +00:00
|
|
|
@abstractmethod
|
|
|
|
def get_path(self, image_type: ImageType, image_name: str) -> str:
|
2023-05-23 12:57:29 +00:00
|
|
|
"""Gets an image's path."""
|
|
|
|
pass
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
def validate_path(self, path: str) -> bool:
|
|
|
|
"""Validates an image's path."""
|
2023-05-21 12:15:44 +00:00
|
|
|
pass
|
|
|
|
|
|
|
|
@abstractmethod
|
2023-05-22 05:48:12 +00:00
|
|
|
def get_url(
|
|
|
|
self, image_type: ImageType, image_name: str, thumbnail: bool = False
|
|
|
|
) -> str:
|
2023-05-23 12:57:29 +00:00
|
|
|
"""Gets an image's or thumbnail's URL."""
|
2023-05-21 12:15:44 +00:00
|
|
|
pass
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
def get_many(
|
|
|
|
self,
|
|
|
|
page: int = 0,
|
|
|
|
per_page: int = 10,
|
2023-05-27 08:32:16 +00:00
|
|
|
image_type: Optional[ImageType] = None,
|
|
|
|
image_category: Optional[ImageCategory] = None,
|
|
|
|
is_intermediate: Optional[bool] = None,
|
|
|
|
show_in_gallery: Optional[bool] = None,
|
2023-05-21 12:15:44 +00:00
|
|
|
) -> PaginatedResults[ImageDTO]:
|
|
|
|
"""Gets a paginated list of image DTOs."""
|
|
|
|
pass
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
def delete(self, image_type: ImageType, image_name: str):
|
|
|
|
"""Deletes an image."""
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2023-05-17 09:13:53 +00:00
|
|
|
class ImageServiceDependencies:
|
2023-05-21 12:15:44 +00:00
|
|
|
"""Service dependencies for the ImageService."""
|
2023-05-17 09:13:53 +00:00
|
|
|
|
2023-05-21 10:05:33 +00:00
|
|
|
records: ImageRecordStorageBase
|
|
|
|
files: ImageFileStorageBase
|
2023-05-17 09:13:53 +00:00
|
|
|
metadata: MetadataServiceBase
|
|
|
|
urls: UrlServiceBase
|
2023-05-21 10:24:59 +00:00
|
|
|
logger: Logger
|
2023-05-26 23:10:02 +00:00
|
|
|
names: NameServiceBase
|
2023-05-22 05:48:12 +00:00
|
|
|
graph_execution_manager: ItemStorageABC["GraphExecutionState"]
|
2023-05-17 09:13:53 +00:00
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
2023-05-21 10:05:33 +00:00
|
|
|
image_record_storage: ImageRecordStorageBase,
|
|
|
|
image_file_storage: ImageFileStorageBase,
|
|
|
|
metadata: MetadataServiceBase,
|
|
|
|
url: UrlServiceBase,
|
2023-05-21 10:24:59 +00:00
|
|
|
logger: Logger,
|
2023-05-26 23:10:02 +00:00
|
|
|
names: NameServiceBase,
|
2023-05-22 05:48:12 +00:00
|
|
|
graph_execution_manager: ItemStorageABC["GraphExecutionState"],
|
2023-05-17 09:13:53 +00:00
|
|
|
):
|
2023-05-21 10:05:33 +00:00
|
|
|
self.records = image_record_storage
|
|
|
|
self.files = image_file_storage
|
|
|
|
self.metadata = metadata
|
|
|
|
self.urls = url
|
2023-05-21 10:24:59 +00:00
|
|
|
self.logger = logger
|
2023-05-26 23:10:02 +00:00
|
|
|
self.names = names
|
2023-05-22 05:48:12 +00:00
|
|
|
self.graph_execution_manager = graph_execution_manager
|
2023-05-17 09:13:53 +00:00
|
|
|
|
|
|
|
|
2023-05-21 12:15:44 +00:00
|
|
|
class ImageService(ImageServiceABC):
|
2023-05-17 09:13:53 +00:00
|
|
|
_services: ImageServiceDependencies
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
2023-05-21 10:05:33 +00:00
|
|
|
image_record_storage: ImageRecordStorageBase,
|
|
|
|
image_file_storage: ImageFileStorageBase,
|
|
|
|
metadata: MetadataServiceBase,
|
|
|
|
url: UrlServiceBase,
|
2023-05-21 10:24:59 +00:00
|
|
|
logger: Logger,
|
2023-05-26 23:10:02 +00:00
|
|
|
names: NameServiceBase,
|
2023-05-22 05:48:12 +00:00
|
|
|
graph_execution_manager: ItemStorageABC["GraphExecutionState"],
|
2023-05-17 09:13:53 +00:00
|
|
|
):
|
|
|
|
self._services = ImageServiceDependencies(
|
2023-05-21 10:05:33 +00:00
|
|
|
image_record_storage=image_record_storage,
|
|
|
|
image_file_storage=image_file_storage,
|
|
|
|
metadata=metadata,
|
|
|
|
url=url,
|
2023-05-21 10:24:59 +00:00
|
|
|
logger=logger,
|
2023-05-26 23:10:02 +00:00
|
|
|
names=names,
|
2023-05-22 05:48:12 +00:00
|
|
|
graph_execution_manager=graph_execution_manager,
|
2023-05-17 09:13:53 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
def create(
|
|
|
|
self,
|
|
|
|
image: PILImageType,
|
|
|
|
image_type: ImageType,
|
|
|
|
image_category: ImageCategory,
|
2023-05-21 10:05:33 +00:00
|
|
|
node_id: Optional[str] = None,
|
|
|
|
session_id: Optional[str] = None,
|
2023-05-25 13:47:18 +00:00
|
|
|
is_intermediate: bool = False,
|
2023-05-27 08:32:16 +00:00
|
|
|
show_in_gallery: bool = True,
|
2023-05-21 10:05:33 +00:00
|
|
|
) -> ImageDTO:
|
2023-05-23 08:59:43 +00:00
|
|
|
if image_type not in ImageType:
|
|
|
|
raise InvalidImageTypeException
|
|
|
|
|
|
|
|
if image_category not in ImageCategory:
|
|
|
|
raise InvalidImageCategoryException
|
|
|
|
|
2023-05-26 23:10:02 +00:00
|
|
|
image_name = self._services.names.create_image_name()
|
2023-05-17 09:13:53 +00:00
|
|
|
|
2023-05-22 05:48:12 +00:00
|
|
|
metadata = self._get_metadata(session_id, node_id)
|
2023-05-21 12:15:44 +00:00
|
|
|
|
2023-05-23 08:59:43 +00:00
|
|
|
(width, height) = image.size
|
|
|
|
|
2023-05-17 09:13:53 +00:00
|
|
|
try:
|
|
|
|
# TODO: Consider using a transaction here to ensure consistency between storage and database
|
2023-05-23 08:59:43 +00:00
|
|
|
created_at = self._services.records.save(
|
|
|
|
# Non-nullable fields
|
2023-05-17 09:13:53 +00:00
|
|
|
image_name=image_name,
|
|
|
|
image_type=image_type,
|
|
|
|
image_category=image_category,
|
2023-05-23 08:59:43 +00:00
|
|
|
width=width,
|
|
|
|
height=height,
|
2023-05-25 13:47:18 +00:00
|
|
|
# Meta fields
|
|
|
|
is_intermediate=is_intermediate,
|
2023-05-27 08:32:16 +00:00
|
|
|
show_in_gallery=show_in_gallery,
|
2023-05-23 08:59:43 +00:00
|
|
|
# Nullable fields
|
2023-05-17 09:13:53 +00:00
|
|
|
node_id=node_id,
|
|
|
|
session_id=session_id,
|
|
|
|
metadata=metadata,
|
2023-05-23 08:59:43 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
self._services.files.save(
|
|
|
|
image_type=image_type,
|
|
|
|
image_name=image_name,
|
|
|
|
image=image,
|
|
|
|
metadata=metadata,
|
2023-05-17 09:13:53 +00:00
|
|
|
)
|
|
|
|
|
2023-05-21 10:05:33 +00:00
|
|
|
image_url = self._services.urls.get_image_url(image_type, image_name)
|
2023-05-21 12:44:16 +00:00
|
|
|
thumbnail_url = self._services.urls.get_image_url(
|
|
|
|
image_type, image_name, True
|
2023-05-21 10:24:59 +00:00
|
|
|
)
|
2023-05-17 09:13:53 +00:00
|
|
|
|
2023-05-21 10:05:33 +00:00
|
|
|
return ImageDTO(
|
2023-05-23 08:59:43 +00:00
|
|
|
# Non-nullable fields
|
2023-05-17 09:13:53 +00:00
|
|
|
image_name=image_name,
|
|
|
|
image_type=image_type,
|
|
|
|
image_category=image_category,
|
2023-05-23 08:59:43 +00:00
|
|
|
width=width,
|
|
|
|
height=height,
|
|
|
|
# Nullable fields
|
2023-05-17 09:13:53 +00:00
|
|
|
node_id=node_id,
|
|
|
|
session_id=session_id,
|
|
|
|
metadata=metadata,
|
2023-05-23 08:59:43 +00:00
|
|
|
# Meta fields
|
|
|
|
created_at=created_at,
|
|
|
|
updated_at=created_at, # this is always the same as the created_at at this time
|
|
|
|
deleted_at=None,
|
2023-05-25 13:47:18 +00:00
|
|
|
is_intermediate=is_intermediate,
|
2023-05-27 08:32:16 +00:00
|
|
|
show_in_gallery=show_in_gallery,
|
2023-05-23 08:59:43 +00:00
|
|
|
# Extra non-nullable fields for DTO
|
2023-05-17 09:13:53 +00:00
|
|
|
image_url=image_url,
|
|
|
|
thumbnail_url=thumbnail_url,
|
|
|
|
)
|
2023-05-23 08:59:43 +00:00
|
|
|
except ImageRecordSaveException:
|
2023-05-21 10:24:59 +00:00
|
|
|
self._services.logger.error("Failed to save image record")
|
2023-05-17 09:13:53 +00:00
|
|
|
raise
|
2023-05-23 08:59:43 +00:00
|
|
|
except ImageFileSaveException:
|
2023-05-21 10:24:59 +00:00
|
|
|
self._services.logger.error("Failed to save image file")
|
2023-05-17 09:13:53 +00:00
|
|
|
raise
|
2023-05-21 12:15:44 +00:00
|
|
|
except Exception as e:
|
|
|
|
self._services.logger.error("Problem saving image record and file")
|
|
|
|
raise e
|
2023-05-17 09:13:53 +00:00
|
|
|
|
2023-05-25 13:47:18 +00:00
|
|
|
def update(
|
|
|
|
self,
|
|
|
|
image_type: ImageType,
|
|
|
|
image_name: str,
|
|
|
|
changes: ImageRecordChanges,
|
|
|
|
) -> ImageDTO:
|
|
|
|
try:
|
|
|
|
self._services.records.update(image_name, image_type, changes)
|
|
|
|
return self.get_dto(image_type, image_name)
|
|
|
|
except ImageRecordSaveException:
|
|
|
|
self._services.logger.error("Failed to update image record")
|
|
|
|
raise
|
|
|
|
except Exception as e:
|
|
|
|
self._services.logger.error("Problem updating image record")
|
|
|
|
raise e
|
|
|
|
|
2023-05-17 09:13:53 +00:00
|
|
|
def get_pil_image(self, image_type: ImageType, image_name: str) -> PILImageType:
|
|
|
|
try:
|
2023-05-21 10:05:33 +00:00
|
|
|
return self._services.files.get(image_type, image_name)
|
2023-05-23 08:59:43 +00:00
|
|
|
except ImageFileNotFoundException:
|
2023-05-21 10:24:59 +00:00
|
|
|
self._services.logger.error("Failed to get image file")
|
2023-05-17 09:13:53 +00:00
|
|
|
raise
|
2023-05-21 12:15:44 +00:00
|
|
|
except Exception as e:
|
|
|
|
self._services.logger.error("Problem getting image file")
|
|
|
|
raise e
|
2023-05-17 09:13:53 +00:00
|
|
|
|
|
|
|
def get_record(self, image_type: ImageType, image_name: str) -> ImageRecord:
|
|
|
|
try:
|
2023-05-21 10:05:33 +00:00
|
|
|
return self._services.records.get(image_type, image_name)
|
2023-05-23 08:59:43 +00:00
|
|
|
except ImageRecordNotFoundException:
|
2023-05-21 12:15:44 +00:00
|
|
|
self._services.logger.error("Image record not found")
|
2023-05-21 10:05:33 +00:00
|
|
|
raise
|
2023-05-21 12:15:44 +00:00
|
|
|
except Exception as e:
|
|
|
|
self._services.logger.error("Problem getting image record")
|
|
|
|
raise e
|
|
|
|
|
2023-05-21 10:05:33 +00:00
|
|
|
def get_dto(self, image_type: ImageType, image_name: str) -> ImageDTO:
|
|
|
|
try:
|
|
|
|
image_record = self._services.records.get(image_type, image_name)
|
|
|
|
|
|
|
|
image_dto = image_record_to_dto(
|
|
|
|
image_record,
|
|
|
|
self._services.urls.get_image_url(image_type, image_name),
|
2023-05-21 12:44:16 +00:00
|
|
|
self._services.urls.get_image_url(image_type, image_name, True),
|
2023-05-17 09:13:53 +00:00
|
|
|
)
|
2023-05-21 10:05:33 +00:00
|
|
|
|
|
|
|
return image_dto
|
2023-05-23 08:59:43 +00:00
|
|
|
except ImageRecordNotFoundException:
|
2023-05-21 12:15:44 +00:00
|
|
|
self._services.logger.error("Image record not found")
|
2023-05-17 09:13:53 +00:00
|
|
|
raise
|
2023-05-21 12:15:44 +00:00
|
|
|
except Exception as e:
|
|
|
|
self._services.logger.error("Problem getting image DTO")
|
|
|
|
raise e
|
2023-05-17 09:13:53 +00:00
|
|
|
|
2023-05-22 05:48:12 +00:00
|
|
|
def get_path(
|
|
|
|
self, image_type: ImageType, image_name: str, thumbnail: bool = False
|
|
|
|
) -> str:
|
|
|
|
try:
|
|
|
|
return self._services.files.get_path(image_type, image_name, thumbnail)
|
|
|
|
except Exception as e:
|
|
|
|
self._services.logger.error("Problem getting image path")
|
|
|
|
raise e
|
|
|
|
|
2023-05-23 12:57:29 +00:00
|
|
|
def validate_path(self, path: str) -> bool:
|
|
|
|
try:
|
|
|
|
return self._services.files.validate_path(path)
|
|
|
|
except Exception as e:
|
|
|
|
self._services.logger.error("Problem validating image path")
|
|
|
|
raise e
|
|
|
|
|
2023-05-22 05:48:12 +00:00
|
|
|
def get_url(
|
|
|
|
self, image_type: ImageType, image_name: str, thumbnail: bool = False
|
|
|
|
) -> str:
|
|
|
|
try:
|
|
|
|
return self._services.urls.get_image_url(image_type, image_name, thumbnail)
|
|
|
|
except Exception as e:
|
|
|
|
self._services.logger.error("Problem getting image path")
|
|
|
|
raise e
|
|
|
|
|
2023-05-17 09:13:53 +00:00
|
|
|
def get_many(
|
|
|
|
self,
|
|
|
|
page: int = 0,
|
|
|
|
per_page: int = 10,
|
2023-05-27 08:32:16 +00:00
|
|
|
image_type: Optional[ImageType] = None,
|
|
|
|
image_category: Optional[ImageCategory] = None,
|
|
|
|
is_intermediate: Optional[bool] = None,
|
|
|
|
show_in_gallery: Optional[bool] = None,
|
2023-05-21 10:05:33 +00:00
|
|
|
) -> PaginatedResults[ImageDTO]:
|
2023-05-17 09:13:53 +00:00
|
|
|
try:
|
2023-05-21 10:05:33 +00:00
|
|
|
results = self._services.records.get_many(
|
2023-05-27 08:32:16 +00:00
|
|
|
page,
|
|
|
|
per_page,
|
2023-05-21 10:05:33 +00:00
|
|
|
image_type,
|
|
|
|
image_category,
|
2023-05-26 23:17:06 +00:00
|
|
|
is_intermediate,
|
2023-05-27 08:32:16 +00:00
|
|
|
show_in_gallery,
|
2023-05-17 09:13:53 +00:00
|
|
|
)
|
|
|
|
|
2023-05-21 10:05:33 +00:00
|
|
|
image_dtos = list(
|
|
|
|
map(
|
|
|
|
lambda r: image_record_to_dto(
|
|
|
|
r,
|
2023-05-27 08:32:16 +00:00
|
|
|
self._services.urls.get_image_url(r.image_type, r.image_name),
|
2023-05-21 12:44:16 +00:00
|
|
|
self._services.urls.get_image_url(
|
2023-05-27 08:32:16 +00:00
|
|
|
r.image_type, r.image_name, True
|
2023-05-21 12:44:16 +00:00
|
|
|
),
|
2023-05-21 10:05:33 +00:00
|
|
|
),
|
|
|
|
results.items,
|
2023-05-17 09:13:53 +00:00
|
|
|
)
|
2023-05-21 10:05:33 +00:00
|
|
|
)
|
2023-05-17 09:13:53 +00:00
|
|
|
|
2023-05-21 10:05:33 +00:00
|
|
|
return PaginatedResults[ImageDTO](
|
|
|
|
items=image_dtos,
|
|
|
|
page=results.page,
|
|
|
|
pages=results.pages,
|
|
|
|
per_page=results.per_page,
|
|
|
|
total=results.total,
|
|
|
|
)
|
2023-05-17 09:13:53 +00:00
|
|
|
except Exception as e:
|
2023-05-21 12:15:44 +00:00
|
|
|
self._services.logger.error("Problem getting paginated image DTOs")
|
|
|
|
raise e
|
|
|
|
|
|
|
|
def delete(self, image_type: ImageType, image_name: str):
|
|
|
|
try:
|
|
|
|
self._services.files.delete(image_type, image_name)
|
|
|
|
self._services.records.delete(image_type, image_name)
|
2023-05-23 08:59:43 +00:00
|
|
|
except ImageRecordDeleteException:
|
2023-05-21 12:15:44 +00:00
|
|
|
self._services.logger.error(f"Failed to delete image record")
|
|
|
|
raise
|
2023-05-23 08:59:43 +00:00
|
|
|
except ImageFileDeleteException:
|
2023-05-21 12:15:44 +00:00
|
|
|
self._services.logger.error(f"Failed to delete image file")
|
|
|
|
raise
|
|
|
|
except Exception as e:
|
|
|
|
self._services.logger.error("Problem deleting image record and file")
|
2023-05-17 09:13:53 +00:00
|
|
|
raise e
|
|
|
|
|
2023-05-22 05:48:12 +00:00
|
|
|
def _get_metadata(
|
|
|
|
self, session_id: Optional[str] = None, node_id: Optional[str] = None
|
|
|
|
) -> Union[ImageMetadata, None]:
|
|
|
|
"""Get the metadata for a node."""
|
|
|
|
metadata = None
|
|
|
|
|
|
|
|
if node_id is not None and session_id is not None:
|
|
|
|
session = self._services.graph_execution_manager.get(session_id)
|
|
|
|
metadata = self._services.metadata.create_image_metadata(session, node_id)
|
|
|
|
|
|
|
|
return metadata
|