Skip to content

Instantly share code, notes, and snippets.

@JanChec
Created February 9, 2020 16:22
Show Gist options
  • Save JanChec/edc052ac77332f8172486b95d6a01fb8 to your computer and use it in GitHub Desktop.
Save JanChec/edc052ac77332f8172486b95d6a01fb8 to your computer and use it in GitHub Desktop.
PotPlayer bookmarks file (.pbf) - Python classess for reading/writing
"""
PotPlayer Bookmark File (.pbf)
"""
from dataclasses import dataclass
import datetime
import pathlib
import re
import typing
class InvalidPbfFormat(Exception):
pass
@dataclass
class Bookmark:
original_index: int
time_in_milliseconds: int
name: str
thumbnail_raw: str
@property
def time_point(self) -> datetime.timedelta:
return datetime.timedelta(milliseconds=self.time_in_milliseconds)
@classmethod
def time_point_to_milliseconds(cls, time_point: datetime.timedelta) -> int:
return (time_point.seconds * 1000
+ time_point.microseconds // 1000)
class ReaderWriter:
EXPECTED_FIRST_LINE = '[Bookmark]'
BOOKMARK_REGEX = (r'(?P<index>[0-9]+)'
r'=(?P<time_in_milliseconds>[0-9]+)'
r'\*(?P<name>[^*]+)'
r'\*(?P<thumbnail_raw>.*)')
BOOKMARK_PATTERN = '{index}={time_in_milliseconds}*{name}*{thumbnail_raw}'
FINAL_SENTRY_REGEX = r'[0-9]+='
FINAL_SENTRY_PATTERN = '{index}='
EXTRA_LINES_COUNT = 2
@classmethod
def read(cls, path: pathlib.Path) -> typing.List[Bookmark]:
contents = path.read_text(encoding='utf16')
cls.validate(contents=contents)
_, bookmark_lines, _ = cls._split_file(contents)
return list(cls._parse_bookmarks(bookmark_lines))
@classmethod
def _parse_bookmarks(cls, bookmark_lines) -> typing.Generator[Bookmark, None, None]:
for bookmark_line in bookmark_lines:
match = re.fullmatch(cls.BOOKMARK_REGEX, bookmark_line)
yield Bookmark(
original_index=int(match.group('index')),
time_in_milliseconds=int(match.group('time_in_milliseconds')),
name=match.group('name'),
thumbnail_raw=match.group('thumbnail_raw'),
)
@classmethod
def write(cls, path: pathlib.Path, bookmarks: typing.Sequence[Bookmark]):
raw_file_contents = cls._produce_raw_file_contents(bookmarks)
cls.validate(raw_file_contents)
path.write_text(raw_file_contents)
@classmethod
def _produce_raw_file_contents(cls, bookmarks: typing.Sequence[Bookmark]) -> str:
first_line = cls.EXPECTED_FIRST_LINE
bookmark_lines = list(cls._unparse_bookmarks(bookmarks))
sentry_line = cls.FINAL_SENTRY_PATTERN.format(index=str(len(bookmarks)))
extra_empty_lines = [''] * cls.EXTRA_LINES_COUNT
lines = [first_line] + bookmark_lines + [sentry_line] + extra_empty_lines
return '\n'.join(lines)
@classmethod
def _unparse_bookmarks(cls, bookmarks: typing.Iterable[Bookmark]) -> typing.Generator[str, None, None]:
for index, bookmark in enumerate(bookmarks):
yield cls.BOOKMARK_PATTERN.format(
index=index,
time_in_milliseconds=bookmark.time_in_milliseconds,
name=bookmark.name,
thumbnail_raw=bookmark.thumbnail_raw,
)
@classmethod
def validate(cls, contents: str):
first_line, bookmark_lines, final_sentry_line = cls._split_file(contents)
if first_line != cls.EXPECTED_FIRST_LINE:
raise InvalidPbfFormat(first_line, cls.EXPECTED_FIRST_LINE)
for bookmark_line in bookmark_lines:
match = re.fullmatch(cls.BOOKMARK_REGEX, bookmark_line)
if not match:
raise InvalidPbfFormat
if not re.fullmatch(cls.FINAL_SENTRY_REGEX, final_sentry_line):
raise InvalidPbfFormat(final_sentry_line)
@staticmethod
def _split_file(contents):
nonempty_lines = [line for line in contents.splitlines()
if line]
first_line = nonempty_lines[0]
bookmark_lines = nonempty_lines[1:-1]
final_sentry_line = nonempty_lines[-1]
return first_line, bookmark_lines, final_sentry_line
if __name__ == '__main__':
def for_example_a_video_is_split_and_we_want_its_bookmarks_split_too():
def _run():
directory = pathlib.Path('/home/panther/')
source_pbf = directory / 'Nyan Cat 24h loop.pbf'
first_half_pbf = directory / 'Nyan Cat 24h loop - part 1 of 2.pbf'
second_half_pbf = directory / 'Nyan Cat 24h loop - part 2 of 2.pbf'
split_time_point = datetime.timedelta(hours=12)
bookmarks = ReaderWriter.read(path=source_pbf)
first_half_bookmarks = [bookmark for bookmark in bookmarks
if bookmark.time_point < split_time_point]
second_half_bookmarks = [_create_adjusted_bookmark(bookmark, split_time_point)
for bookmark in bookmarks
if bookmark.time_point >= split_time_point]
ReaderWriter.write(path=first_half_pbf, bookmarks=first_half_bookmarks)
ReaderWriter.write(path=second_half_pbf, bookmarks=second_half_bookmarks)
def _create_adjusted_bookmark(bookmark: Bookmark,
new_time_start: datetime.timedelta) -> Bookmark:
assert new_time_start <= bookmark.time_point
adjusted_time_point = bookmark.time_point - new_time_start
return Bookmark(
original_index=bookmark.original_index,
time_in_milliseconds=Bookmark.time_point_to_milliseconds(adjusted_time_point),
name=bookmark.name,
thumbnail_raw=bookmark.thumbnail_raw,
)
_run()
for_example_a_video_is_split_and_we_want_its_bookmarks_split_too()
@JanChec
Copy link
Author

JanChec commented Feb 9, 2020

If you know how thumbnails are encoded there - please drop me a line.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment