Skip to content

Instantly share code, notes, and snippets.

@neko-neko-nyan
Last active May 18, 2022 09:12
Show Gist options
  • Save neko-neko-nyan/091a3903f967f9e6ae27e844f707eac8 to your computer and use it in GitHub Desktop.
Save neko-neko-nyan/091a3903f967f9e6ae27e844f707eac8 to your computer and use it in GitHub Desktop.
Writing XSPF playlists with VLC extensions in Python
import typing
class Node:
def write(self, f: 'XMLWriter'):
raise NotImplementedError()
class WithTags:
def __init__(self, **kwargs):
self.title = kwargs.get('title')
self.creator = kwargs.get('creator')
self.annotation = kwargs.get('annotation')
self.image = kwargs.get('image')
self.options = [(name, value) for name, value in kwargs.items() if name[0] == ':']
def _write_tags(self, f):
f.simple_tags({
'title': self.title,
'creator': self.creator,
'annotation': self.annotation,
'image': self.image,
})
def _write_options(self, f):
for name, value in self.options:
f.simple('vlc:option', f'{name}={value}')
def option(self, name, value):
self.options.append((name, value))
return self
class Container:
def __init__(self):
self.children = [] # type: typing.List[Node]
def add(self, item: Node):
self.children.append(item)
return item
@property
def all_tracks(self) -> 'typing.List[Track]':
res = []
for i in self.children:
if isinstance(i, Container):
res += i.all_tracks
elif isinstance(i, Track):
res.append(i)
else:
raise TypeError('Only Containers and Tracks may be placed into Container')
return res
def _write_children(self, f):
for i in self.children:
i.write(f)
def add_folder(self, title):
return self.add(Folder(title))
def add_track(self, **kwargs):
return self.add(Track(**kwargs))
class Track(Node, WithTags):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.location = kwargs.get('location')
self.info = kwargs.get('info')
self.album = kwargs.get('album')
self.trackNum = kwargs.get('trackNum')
self.duration = kwargs.get('duration')
self._tid = None
def write_header(self, f, tid):
self._tid = tid
f.begin('track')
self._write_tags(f)
f.simple_tags({
'location': self.location,
'info': self.info,
'album': self.album,
'trackNum': self.trackNum,
'duration': self.duration,
})
f.begin('extension', application="http://www.videolan.org/vlc/playlist/0")
self._write_options(f)
f.simple('vlc:id', tid)
f.end()
f.end()
def write(self, f):
f.simple('vlc:item', tid=self._tid)
class Folder(Node, Container):
def __init__(self, title):
super().__init__()
self.title = title
def write(self, f):
f.begin('vlc:node', title=self.title)
self._write_children(f)
f.end()
class Playlist(Node, WithTags, Container):
def __init__(self, **kwargs):
WithTags.__init__(self, **kwargs)
Container.__init__(self)
def write(self, f):
f.begin('playlist', version="1", xmlns="http://xspf.org/ns/0/",
**{'xmlns:vlc': "http://www.videolan.org/vlc/playlist/ns/0/"})
self._write_tags(f)
f.begin('trackList')
for tid, i in enumerate(self.all_tracks):
i.write_header(f, tid)
f.end()
f.begin('extension', application="http://www.videolan.org/vlc/playlist/0")
self._write_options(f)
self._write_children(f)
f.end()
f.end()
class XMLWriter:
ESCAPING_MAP = {
"<": "&lt;",
">": "&gt;",
"\"": "&quot;",
"'": "&apos;",
"&": "&amp;",
}
def __init__(self, f):
self._stream = f
self._path = []
self._stream.write('<?xml version="1.0" encoding="UTF-8"?>')
def begin(self, name: str, **kwargs):
self._path.append(name)
self._stream.write(f'<{name} {" ".join(self._make_attrs(kwargs))}>')
def end(self):
name = self._path.pop()
self._stream.write(f'</{name}>')
def simple(self, name, value=None, **kwargs):
attrs = " ".join(self._make_attrs(kwargs))
if value is None:
self._stream.write(f'<{name} {attrs} />')
else:
self._stream.write(f'<{name} {attrs}>{self._escape(value)}</{name}>')
def simple_tags(self, dct: dict):
for name, value in dct.items():
if value is not None:
self.simple(name, value)
def _make_attrs(self, dct):
for name, value in dct.items():
if value is not None:
yield f'{name}="{self._escape(value)}"'
def _escape(self, data):
data = str(data)
for before, after in self.ESCAPING_MAP.items():
data = data.replace(before, after)
return data
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment