+class TagSet:
+ """Collection of tags as used in VideoFile.tags."""
+
+ def __init__(self, tags: Optional[set[str]] = None) -> None:
+ self._tags = tags if tags else set()
+
+ def __iter__(self) -> Generator:
+ yield from self._tags
+
+ def add(self, other_tags: Self) -> None:
+ """To self, add tags of other_tags."""
+ # pylint:disable=protected-access
+ self._tags = self._tags | other_tags._tags
+
+ def all_not_in(self, other_tags: Self) -> Self:
+ """Return all tags of self _not_ in other_tags."""
+ # pylint:disable=protected-access
+ return self.__class__({t for t in self._tags
+ if t not in other_tags._tags})
+
+ def all_also_in(self, other_tags: Self) -> Self:
+ """Return all tags of self _also_ in other_tags"""
+ # pylint:disable=protected-access
+ return self.__class__({t for t in self._tags
+ if t in other_tags._tags})
+
+ def are_all_in(self, other_tags: Self) -> bool:
+ """Return if all tags of self are also in other_tags."""
+ return self.empty or self.all_not_in(other_tags).empty
+
+ def whitelisted(self, whitelist: Self) -> Self:
+ """Return self filtered by whitelist; if empty, return all."""
+ if whitelist.empty:
+ return self
+ return self.all_also_in(whitelist)
+
+ @property
+ def empty(self) -> bool:
+ """Return if self empty."""
+ return 0 == len(self._tags)
+
+ @property
+ def as_str_list(self) -> list[str]:
+ """Return self list of strings."""
+ return [str(t) for t in self._tags]
+
+ @property
+ def joined(self) -> str:
+ """Return self as string of comma-separated tags."""
+ return ','.join(self.as_str_list)
+
+ @classmethod
+ def from_joined(cls, joined: str) -> Self:
+ """Build self from string of comma-separated tags."""
+ return cls({t for t in joined.split(',') if t})
+
+ @classmethod
+ def from_str_list(cls, str_list: list[str]) -> Self:
+ """Build self from list of tag strings."""
+ return cls(set(str_list))
+
+