# template paths
_PATH_TEMPLATES = PATH_APP_DATA.joinpath('templates')
-_NAME_TEMPLATE_QUERIES = Path('yt_queries.tmpl')
-_NAME_TEMPLATE_RESULTS = Path('yt_results.tmpl')
-_NAME_TEMPLATE_FILES = Path('files.tmpl')
_NAME_TEMPLATE_FILE_DATA = Path('file_data.tmpl')
-_NAME_TEMPLATE_YT_VIDEO = Path('yt_result.tmpl')
+_NAME_TEMPLATE_FILES = Path('files.tmpl')
_NAME_TEMPLATE_PLAYLIST = Path('playlist.tmpl')
+_NAME_TEMPLATE_YT_QUERIES = Path('yt_queries.tmpl')
+_NAME_TEMPLATE_YT_RESULT = Path('yt_result.tmpl')
+_NAME_TEMPLATE_YT_RESULTS = Path('yt_results.tmpl')
# page names
PAGE_NAMES: dict[str, Path] = {
'playlist': Path('playlist'),
'purge': Path('purge'),
'thumbnails': Path('thumbnails'),
- 'yt_result': Path('yt_result'),
+ 'yt_queries': Path('yt_queries'),
'yt_query': Path('yt_query'),
- 'yt_queries': Path('yt_queries')
+ 'yt_result': Path('yt_result')
}
# misc
_HEADER_APP_JSON = 'application/json'
+class Server(ThreadingHTTPServer):
+ """Extension of parent server providing for Player and DownloadsManager."""
+
+ def __init__(self, config: Config, *args, **kwargs) -> None:
+ super().__init__((config.host, config.port), _TaskHandler,
+ *args, **kwargs)
+ self.config = config
+ self.jinja = JinjaEnv(loader=JinjaFSLoader(_PATH_TEMPLATES))
+ self.player = Player(config.whitelist_tags_display,
+ config.whitelist_tags_prefilter,
+ config.needed_tags_prefilter)
+ self.downloads = DownloadsManager()
+ self.downloads.clean_unfinished()
+ self.downloads.start_thread()
+
+
class _ReqMap:
"""Wrapper over dictionary-like HTTP postings."""
yield k
-class Server(ThreadingHTTPServer):
- """Extension of parent server providing for Player and DownloadsManager."""
-
- def __init__(self, config: Config, *args, **kwargs) -> None:
- super().__init__((config.host, config.port), _TaskHandler,
- *args, **kwargs)
- self.config = config
- self.jinja = JinjaEnv(loader=JinjaFSLoader(_PATH_TEMPLATES))
- self.player = Player(config.whitelist_tags_display,
- config.whitelist_tags_prefilter,
- config.needed_tags_prefilter)
- self.downloads = DownloadsManager()
- self.downloads.clean_unfinished()
- self.downloads.start_thread()
-
-
class _TaskHandler(BaseHTTPRequestHandler):
"""Handler for GET and POST requests to our server."""
server: Server
postvars = _ReqMap(
self.rfile.read(int(self.headers['content-length'])).decode(),
_HEADER_APP_JSON == self.headers[_HEADER_CONTENT_TYPE])
- if PAGE_NAMES['files'] == page_name:
- self._receive_files_command(postvars)
- elif PAGE_NAMES['file'] == page_name:
+ if PAGE_NAMES['file'] == page_name:
self._receive_file_data(Hash.from_b64(toks_url[2]), postvars)
- elif PAGE_NAMES['yt_queries'] == page_name:
- self._receive_yt_query(QueryText(postvars.first_for('query')))
+ elif PAGE_NAMES['files'] == page_name:
+ self._receive_files_command(postvars)
elif PAGE_NAMES['player'] == page_name:
self._receive_player_command(postvars)
elif PAGE_NAMES['purge'] == page_name:
self._purge_deleted_files()
+ elif PAGE_NAMES['yt_queries'] == page_name:
+ self._receive_yt_query(QueryText(postvars.first_for('query')))
- def _purge_deleted_files(self) -> None:
+ def _receive_file_data(self, digest: Hash, postvars: _ReqMap) -> None:
+ if not (self.server.config.allow_file_edit # also if whitelist, …
+ and self.server.config.whitelist_tags_display.empty):
+ self._send_http('no way', code=403) # … cuz input form under …
+ return # … this display filter might have suppressed set tags
with DbConn() as conn:
- VideoFile.purge_deleteds(conn)
- self.server.player.load_files_and_mpv()
+ file = VideoFile.get_one(conn, digest)
+ if postvars.has_key('unlink'):
+ file.unlink_locally()
+ file.set_flags({FILE_FLAGS[FlagName(name)]
+ for name in postvars.all_for('flags')})
+ file.tags = TagSet.from_str_list(postvars.all_for('tags'))
+ file.save(conn)
conn.commit()
- self._send_http('OK')
+ file.ensure_absence_if_deleted()
+ self._redirect(Path(postvars.first_for('redir_target')))
+
+ def _receive_files_command(self, postvars: _ReqMap) -> None:
+ for k in postvars.key_starting_with('play_'):
+ with DbConn() as conn:
+ file = VideoFile.get_one(
+ conn, Hash.from_b64(k.split('_', 1)[1]))
+ self.server.player.inject_and_play(file)
+ self._redirect(Path(postvars.first_for('redir_target')))
def _receive_player_command(self, postvars: _ReqMap) -> None:
command = postvars.first_for('command')
postvars.first_for('needed_tags'))
self._send_http('OK')
- def _receive_files_command(self, postvars: _ReqMap) -> None:
- for k in postvars.key_starting_with('play_'):
- with DbConn() as conn:
- file = VideoFile.get_one(
- conn, Hash.from_b64(k.split('_', 1)[1]))
- self.server.player.inject_and_play(file)
- self._redirect(Path(postvars.first_for('redir_target')))
-
- def _receive_file_data(self, digest: Hash, postvars: _ReqMap) -> None:
- if not (self.server.config.allow_file_edit # also if whitelist, …
- and self.server.config.whitelist_tags_display.empty):
- self._send_http('no way', code=403) # … cuz input form under …
- return # … this display filter might have suppressed set tags
+ def _purge_deleted_files(self) -> None:
with DbConn() as conn:
- file = VideoFile.get_one(conn, digest)
- if postvars.has_key('unlink'):
- file.unlink_locally()
- file.set_flags({FILE_FLAGS[FlagName(name)]
- for name in postvars.all_for('flags')})
- file.tags = TagSet.from_str_list(postvars.all_for('tags'))
- file.save(conn)
+ VideoFile.purge_deleteds(conn)
+ self.server.player.load_files_and_mpv()
conn.commit()
- file.ensure_absence_if_deleted()
- self._redirect(Path(postvars.first_for('redir_target')))
+ self._send_http('OK')
def _receive_yt_query(self, query_txt: QueryText) -> None:
with DbConn() as conn:
toks_url = Path(url.path).parts
page_name = Path(toks_url[1] if len(toks_url) > 1 else '')
try:
- if PAGE_NAMES['thumbnails'] == page_name:
- self._send_thumbnail(Path(toks_url[2]))
- elif PAGE_NAMES['download'] == page_name:
+ if PAGE_NAMES['download'] == page_name:
self._send_or_download_video(YoutubeId(toks_url[2]))
- elif PAGE_NAMES['files'] == page_name:
- self._send_files_index(_ReqMap(url.query))
+ elif PAGE_NAMES['events'] == page_name:
+ self._send_events(_ReqMap(url.query))
elif PAGE_NAMES['file'] == page_name:
self._send_file_data(Hash.from_b64(toks_url[2]))
- elif PAGE_NAMES['yt_result'] == page_name:
- self._send_yt_result(YoutubeId(toks_url[2]))
+ elif PAGE_NAMES['files'] == page_name:
+ self._send_files_index(_ReqMap(url.query))
elif PAGE_NAMES['missing'] == page_name:
self._send_missing_json()
- elif PAGE_NAMES['yt_query'] == page_name:
- self._send_yt_query_page(QueryId(toks_url[2]))
+ elif PAGE_NAMES['thumbnails'] == page_name:
+ self._send_thumbnail(Path(toks_url[2]))
+ elif PAGE_NAMES['yt_result'] == page_name:
+ self._send_yt_result(YoutubeId(toks_url[2]))
elif PAGE_NAMES['yt_queries'] == page_name:
self._send_yt_queries_index_and_search()
- elif PAGE_NAMES['events'] == page_name:
- self._send_events(_ReqMap(url.query))
+ elif PAGE_NAMES['yt_query'] == page_name:
+ self._send_yt_query_page(QueryId(toks_url[2]))
else: # e.g. for /
self._send_playlist()
except NotFoundException as e:
tmpl_ctx['page_names'] = PAGE_NAMES
self._send_http(tmpl.render(**tmpl_ctx))
- def _send_thumbnail(self, filename: Path) -> None:
- ensure_expected_dirs([PATH_THUMBNAILS])
- path_thumbnail = PATH_THUMBNAILS.joinpath(filename)
- if not path_thumbnail.exists():
- video_id = filename.stem
- url = f'{_THUMBNAIL_URL_PREFIX}{video_id}{_THUMBNAIL_URL_SUFFIX}'
- try:
- urlretrieve(url, PATH_THUMBNAILS.joinpath(f'{video_id}.jpg'))
- except HTTPError as e:
- if 404 == e.code:
- raise NotFoundException from e
- raise e
- with path_thumbnail.open('rb') as f:
- self._send_http(f.read(), [(_HEADER_CONTENT_TYPE, 'image/jpg')])
-
def _send_or_download_video(self, video_id: YoutubeId) -> None:
try:
with DbConn() as conn:
.joinpath(PAGE_NAMES['yt_result'])
.joinpath(video_id))
- def _send_yt_query_page(self, query_id: QueryId) -> None:
- with DbConn() as conn:
- query = YoutubeQuery.get_one(conn, str(query_id))
- results = YoutubeVideo.get_all_for_query(conn, query_id)
- self._send_rendered_template(_NAME_TEMPLATE_RESULTS,
- {'query': query.text, 'videos': results})
-
- def _send_yt_queries_index_and_search(self) -> None:
- with DbConn() as conn:
- quota_count = QuotaLog.current(conn)
- queries_data = [
- q for q in YoutubeQuery.get_all(conn)
- if q.retrieved_at > self.server.config.queries_cutoff]
- queries_data.sort(key=lambda q: q.retrieved_at, reverse=True)
- self._send_rendered_template(_NAME_TEMPLATE_QUERIES,
- {'queries': queries_data,
- 'quota_count': quota_count,
- 'selected': 'yt_queries'})
-
- def _send_yt_result(self, video_id: YoutubeId) -> None:
- conn = DbConn()
- with DbConn() as conn:
- linked_queries = YoutubeQuery.get_all_for_video(conn, video_id)
- try:
- video_data = YoutubeVideo.get_one(conn, video_id)
- except NotFoundException:
- video_data = YoutubeVideo(video_id)
- file_digest: Optional[str]
- file_path: Optional[Path]
- try:
- file = VideoFile.get_by_yt_id(conn, video_id)
- file_digest = file.digest.b64
- file_path = file.rel_path if file.present else None
- except NotFoundException:
- file_path, file_digest = None, None
- self._send_rendered_template(
- _NAME_TEMPLATE_YT_VIDEO,
- {'video_data': video_data,
- 'is_temp': video_id in self.server.downloads.ids_unfinished,
- 'file_digest': file_digest,
- 'file_path': file_path,
- 'youtube_prefix': YOUTUBE_URL_PREFIX,
- 'queries': linked_queries})
+ def _send_events(self, params: _ReqMap) -> None:
+ self._send_http(headers=[(_HEADER_CONTENT_TYPE, 'text/event-stream'),
+ ('Cache-Control', 'no-cache'),
+ ('Connection', 'keep-alive')])
+ selected: Optional[VideoFile] = None
+ last_sent = ''
+ payload: dict[str, Any] = {}
+ time_last_write = 0.0
+ while True:
+ if not payload and time_last_write < time() - _PING_INTERVAL_S:
+ payload['ping'] = True
+ if payload:
+ payload_encoded = f'data: {json_dumps(payload)}\n\n'.encode()
+ try:
+ self.wfile.write(payload_encoded)
+ self.wfile.flush()
+ except BrokenPipeError:
+ break
+ time_last_write = time()
+ payload.clear()
+ if not self.server.player.current_digest:
+ selected = None
+ elif ((not selected)
+ or (selected.digest != self.server.player.current_digest)):
+ with DbConn() as conn:
+ selected = VideoFile.get_one_with_whitelist_tags_display(
+ conn,
+ self.server.player.current_digest,
+ self.server.config.whitelist_tags_display)
+ if last_sent < self.server.player.last_update:
+ last_sent = self.server.player.last_update
+ title, tags, digest = '', '', ''
+ if selected:
+ tags = selected.tags_showable.joined
+ title = str(selected.rel_path)
+ digest = selected.digest.b64
+ payload['is_running'] = self.server.player.is_running
+ payload['is_playing'] = self.server.player.is_playing
+ payload['can_play'] = self.server.player.can_play
+ payload['title_tags'] = tags
+ payload['title_digest'] = digest
+ payload['title'] = title
+ if params.has_key('playlist'):
+ payload['idx'] = self.server.player.idx
+ payload['playlist_files'] = [
+ {'rel_path': str(f.rel_path), 'digest': f.digest.b64}
+ for f in self.server.player.playlist]
+ else:
+ sleep(_EVENTS_UPDATE_INTERVAL_S)
def _send_file_data(self, digest: Hash) -> None:
with DbConn() as conn:
self._send_http(json_dumps(missing),
headers=[(_HEADER_CONTENT_TYPE, _HEADER_APP_JSON)])
+ def _send_thumbnail(self, filename: Path) -> None:
+ ensure_expected_dirs([PATH_THUMBNAILS])
+ path_thumbnail = PATH_THUMBNAILS.joinpath(filename)
+ if not path_thumbnail.exists():
+ video_id = filename.stem
+ url = f'{_THUMBNAIL_URL_PREFIX}{video_id}{_THUMBNAIL_URL_SUFFIX}'
+ try:
+ urlretrieve(url, PATH_THUMBNAILS.joinpath(f'{video_id}.jpg'))
+ except HTTPError as e:
+ if 404 == e.code:
+ raise NotFoundException from e
+ raise e
+ with path_thumbnail.open('rb') as f:
+ self._send_http(f.read(), [(_HEADER_CONTENT_TYPE, 'image/jpg')])
+
+ def _send_yt_result(self, video_id: YoutubeId) -> None:
+ conn = DbConn()
+ with DbConn() as conn:
+ linked_queries = YoutubeQuery.get_all_for_video(conn, video_id)
+ try:
+ video_data = YoutubeVideo.get_one(conn, video_id)
+ except NotFoundException:
+ video_data = YoutubeVideo(video_id)
+ file_digest: Optional[str]
+ file_path: Optional[Path]
+ try:
+ file = VideoFile.get_by_yt_id(conn, video_id)
+ file_digest = file.digest.b64
+ file_path = file.rel_path if file.present else None
+ except NotFoundException:
+ file_path, file_digest = None, None
+ self._send_rendered_template(
+ _NAME_TEMPLATE_YT_RESULT,
+ {'video_data': video_data,
+ 'is_temp': video_id in self.server.downloads.ids_unfinished,
+ 'file_digest': file_digest,
+ 'file_path': file_path,
+ 'youtube_prefix': YOUTUBE_URL_PREFIX,
+ 'queries': linked_queries})
+
+ def _send_yt_queries_index_and_search(self) -> None:
+ with DbConn() as conn:
+ quota_count = QuotaLog.current(conn)
+ queries_data = [
+ q for q in YoutubeQuery.get_all(conn)
+ if q.retrieved_at > self.server.config.queries_cutoff]
+ queries_data.sort(key=lambda q: q.retrieved_at, reverse=True)
+ self._send_rendered_template(_NAME_TEMPLATE_YT_QUERIES,
+ {'queries': queries_data,
+ 'quota_count': quota_count,
+ 'selected': 'yt_queries'})
+
+ def _send_yt_query_page(self, query_id: QueryId) -> None:
+ with DbConn() as conn:
+ query = YoutubeQuery.get_one(conn, str(query_id))
+ results = YoutubeVideo.get_all_for_query(conn, query_id)
+ self._send_rendered_template(_NAME_TEMPLATE_YT_RESULTS,
+ {'query': query.text, 'videos': results})
+
def _send_playlist(self) -> None:
self._send_rendered_template(
_NAME_TEMPLATE_PLAYLIST,
{'selected': 'playlist',
'filter_path': self.server.player.filter_path,
'needed_tags': self.server.player.needed_tags.joined})
-
- def _send_events(self, params: _ReqMap) -> None:
- self._send_http(headers=[(_HEADER_CONTENT_TYPE, 'text/event-stream'),
- ('Cache-Control', 'no-cache'),
- ('Connection', 'keep-alive')])
- selected: Optional[VideoFile] = None
- last_sent = ''
- payload: dict[str, Any] = {}
- time_last_write = 0.0
- while True:
- if not payload and time_last_write < time() - _PING_INTERVAL_S:
- payload['ping'] = True
- if payload:
- payload_encoded = f'data: {json_dumps(payload)}\n\n'.encode()
- try:
- self.wfile.write(payload_encoded)
- self.wfile.flush()
- except BrokenPipeError:
- break
- time_last_write = time()
- payload.clear()
- if not self.server.player.current_digest:
- selected = None
- elif ((not selected)
- or (selected.digest != self.server.player.current_digest)):
- with DbConn() as conn:
- selected = VideoFile.get_one_with_whitelist_tags_display(
- conn,
- self.server.player.current_digest,
- self.server.config.whitelist_tags_display)
- if last_sent < self.server.player.last_update:
- last_sent = self.server.player.last_update
- title, tags, digest = '', '', ''
- if selected:
- tags = selected.tags_showable.joined
- title = str(selected.rel_path)
- digest = selected.digest.b64
- payload['is_running'] = self.server.player.is_running
- payload['is_playing'] = self.server.player.is_playing
- payload['can_play'] = self.server.player.can_play
- payload['title_tags'] = tags
- payload['title_digest'] = digest
- payload['title'] = title
- if params.has_key('playlist'):
- payload['idx'] = self.server.player.idx
- payload['playlist_files'] = [
- {'rel_path': str(f.rel_path), 'digest': f.digest.b64}
- for f in self.server.player.playlist]
- else:
- sleep(_EVENTS_UPDATE_INTERVAL_S)