From: Christian Heller Date: Sun, 23 Feb 2025 12:10:33 +0000 (+0100) Subject: Live-update download status on /yt_result pages. X-Git-Url: https://plomlompom.com/repos/%7B%7Bprefix%7D%7D/static/%7B%7Bdb.prefix%7D%7D/booking/do_tasks?a=commitdiff_plain;h=refs%2Fheads%2Fmaster;p=ytplom Live-update download status on /yt_result pages. --- diff --git a/src/templates/yt_result.tmpl b/src/templates/yt_result.tmpl index 5216bc1..123b8c2 100644 --- a/src/templates/yt_result.tmpl +++ b/src/templates/yt_result.tmpl @@ -1,6 +1,24 @@ {% extends '_base.tmpl' %} +{% block script %} +events_params += 'download={{video_data.id_}}'; +event_handlers.download = function(data) { + const td = document.getElementById("status"); + td.innerHTML = ""; + if ("absent" == data.status) { + a = new_child_to("a", td, "download?"); + a.href = "/{{page_names.download}}/{{video_data.id_}}"; + } else if ("present" == data.status) { + a = new_child_to("a", td, data.path); + a.href = "/{{page_names.file}}/" + data.digest; + } else { + td.appendChild(document.createTextNode(`${data.status}`)); + } +} +{% endblock %} + + {% block body %} @@ -9,7 +27,7 @@ - +
title:{{video_data.title}}
duration:{{video_data.duration}}
definition:{{video_data.definition}}
YouTube ID:{{video_data.id_}}
download:{% if is_temp %}working on it{% elif file_path %}{{file_path}}{% else %}please do{% endif %}
download:
linked queries: diff --git a/src/ytplom/http.py b/src/ytplom/http.py index 7f446dd..957cdab 100644 --- a/src/ytplom/http.py +++ b/src/ytplom/http.py @@ -126,7 +126,7 @@ class _TaskHandler(PlomHttpHandler): file.set_flags({FILE_FLAGS[FlagName('delete')]}) file.save(conn) conn.commit() - file.ensure_unlinked_if_deleted() + file.ensure_unlinked_if_deleted(conn) self._redirect(Path(self.postvars.first_for('redir_target'))) def _update_file(self) -> None: @@ -255,9 +255,12 @@ class _TaskHandler(PlomHttpHandler): ('Cache-Control', 'no-cache'), ('Connection', 'keep-alive')]) selected: Optional[VideoFile] = None - last_sent = '' + last_updates = {'player': '', 'download': ''} payload: dict[str, Any] = {} time_last_write = 0.0 + subscriptions = ['player'] + list(self.params.as_dict.keys()) + if 'download' in subscriptions: + download_id = self.params.first_for('download') while True: if not payload and time_last_write < time() - _PING_INTERVAL_S: payload['ping'] = {} @@ -277,22 +280,24 @@ class _TaskHandler(PlomHttpHandler): with DbConn() as conn: selected = VideoFile.get_one( conn, self.server.player.current_digest) - if last_sent < self.server.player.last_update: - last_sent = self.server.player.last_update - title, digest, tags = '', '', [] - if selected: - title = str(selected.rel_path) - digest = selected.digest.b64 - tags = selected.tags_showable.as_str_list - payload['player'] = { - 'is_running': self.server.player.is_running, - 'is_playing': self.server.player.is_playing, - 'can_play': self.server.player.can_play, - 'title_tags': tags, - 'title_digest': digest, - 'title': title - } - if self.params.has_key('playlist'): + if last_updates['player'] < self.server.player.last_update: + if 'player' in subscriptions or 'playlist' in subscriptions: + last_updates['player'] = self.server.player.last_update + if 'player' in subscriptions: + title, digest, tags = '', '', [] + if selected: + title = str(selected.rel_path) + digest = selected.digest.b64 + tags = selected.tags_showable.as_str_list + payload['player'] = { + 'is_running': self.server.player.is_running, + 'is_playing': self.server.player.is_playing, + 'can_play': self.server.player.can_play, + 'title_tags': tags, + 'title_digest': digest, + 'title': title + } + if 'playlist' in subscriptions: payload['playlist'] = { 'idx': self.server.player.idx, 'playlist_files': [ @@ -300,7 +305,14 @@ class _TaskHandler(PlomHttpHandler): 'digest': f.digest.b64} for f in self.server.player.playlist] } - else: + if 'download' in subscriptions: + with DbConn() as conn: + last_update = self.server.downloads.last_update_for( + conn, YoutubeId(download_id)) + if last_updates['download'] < last_update['time']: + last_updates['download'] = last_update['time'] + payload['download'] = last_update + if not payload: sleep(_EVENTS_UPDATE_INTERVAL_S) def _send_file_data(self) -> None: diff --git a/src/ytplom/misc.py b/src/ytplom/misc.py index 1d4ade8..5df2502 100644 --- a/src/ytplom/misc.py +++ b/src/ytplom/misc.py @@ -392,16 +392,12 @@ class VideoFile(DbData): """Return if 'delete' flag set.""" return self.is_flag_set(FlagName('delete')) - def ensure_not_deleted(self) -> None: - """If 'delete' flag set, raise appropriate NotFoundException.""" - if self.deleted: - raise NotFoundException('not showing entry marked as deleted') - @classmethod def get_one(cls, conn: DbConn, id_: str | Hash) -> Self: """Extend super by .test_deletion.""" file = super().get_one(conn, id_) - file.ensure_not_deleted() # pylint: disable=no-member + if file.deleted: # pylint: disable=no-member + raise NotFoundException('not showing entry marked as deleted') # NB: mypy recognizes file as VideoFile without below assert and # if-isinstance-else, yet less type-smart pylint only does due to the # latter (also the reason for the disable=no-member above; but wouldn't @@ -423,14 +419,14 @@ class VideoFile(DbData): @classmethod def get_by_yt_id(cls, conn: DbConn, yt_id: YoutubeId) -> Self: - """Return VideoFile of .yt_id.""" - row = conn.exec(f'SELECT * FROM {cls._table_name} WHERE yt_id =', - (yt_id,)).fetchone() - if not row: - raise NotFoundException(f'no entry for file to Youtube ID {yt_id}') - file = cls._from_table_row(row) - file.ensure_not_deleted() - return file + """Return (non-deleted) VideoFile of .yt_id.""" + rows = conn.exec(f'SELECT * FROM {cls._table_name} WHERE yt_id =', + (yt_id,)).fetchall() + for file in [cls._from_table_row(row) for row in rows]: + if not file.deleted: + return file + raise NotFoundException( + f'no undeleted entry for file to Youtube ID {yt_id}') @classmethod def get_filtered(cls, @@ -529,8 +525,10 @@ class VideoFile(DbData): """Return if flag of flag_name is set in flags field.""" return bool(self.flags & FILE_FLAGS[flag_name]) - def ensure_unlinked_if_deleted(self) -> None: - """If 'delete' flag set, ensure no actual file in filesystem.""" + def ensure_unlinked_if_deleted(self, conn: DbConn) -> None: + """If 'delete' flag set and no undeleted owner, ensure unlinked.""" + if self.full_path in [f.full_path for f in self.get_all(conn)]: + return if self.is_flag_set(FlagName('delete')) and self.present: self.unlink_locally() @@ -784,10 +782,12 @@ class Player: class DownloadsManager: """Manages downloading and downloads access.""" + _last_updates: dict[YoutubeId, dict[str, str]] def __init__(self) -> None: self._to_download: list[YoutubeId] = [] ensure_expected_dirs([PATH_DOWNLOADS, PATH_TEMP]) + self._last_updates = {} self._sync_db() def _sync_db(self): @@ -795,20 +795,56 @@ class DownloadsManager: known_paths = [file.rel_path for file in VideoFile.get_all(conn)] old_cwd = Path.cwd() chdir(PATH_DOWNLOADS) - for path in [p for p in Path('.').iterdir() - if p.is_file() and p not in known_paths]: + for path in [p for p in Path('.').iterdir() if p.is_file()]: yt_id = self._id_from_filename(path) - print(f'SYNC: new file {path}, saving to YT ID "{yt_id}".') - file = VideoFile(digest=None, - rel_path=path, - yt_id=yt_id, - tags_str=VideoFile.tags_default.joined) - file.save(conn) + if path not in known_paths: + print(f'SYNC: new file {path}, saving to YT ID "{yt_id}".') + file = VideoFile(digest=None, + rel_path=path, + yt_id=yt_id, + tags_str=VideoFile.tags_default.joined) + file.save(conn) + conn.commit() + if (yt_id not in self._last_updates + or 'present' != self._last_updates[yt_id]['status']): + self._update_status( + yt_id, + 'present', + str(path), + VideoFile.get_by_yt_id(conn, yt_id).digest.b64) for file in VideoFile.get_deleteds(conn): - file.ensure_unlinked_if_deleted() + file.ensure_unlinked_if_deleted(conn) self._files = VideoFile.get_all(conn) chdir(old_cwd) - conn.commit() + + def last_update_for(self, + conn: DbConn, + yt_id: YoutubeId + ) -> dict[str, str]: + """Retrieve ._last_updates[yt_id] but reset to 'absent' if needed.""" + if yt_id in self._last_updates: + if self._last_updates[yt_id]['status'] != 'present': + return self._last_updates[yt_id] + try: + file = VideoFile.get_by_yt_id(conn, yt_id) + if not file.present: + self._update_status(yt_id, 'absent') + except NotFoundException: + self._update_status(yt_id, 'absent') + else: + self._update_status(yt_id, 'absent') + return self._last_updates[yt_id] + + def _update_status(self, + yt_id: YoutubeId, + status: str, + path: str = '', + digest: str = '' + ) -> None: + self._last_updates[yt_id] = {'status': status, 'time': _now_string()} + for k, v in [('path', path), ('digest', digest)]: + if k: + self._last_updates[yt_id] |= {k: v} @staticmethod def _id_from_filename(path: Path) -> YoutubeId: @@ -836,11 +872,13 @@ class DownloadsManager: + [f.full_path for f in self._files])) if video_id not in pre_existing: self._to_download += [video_id] + self._update_status(video_id, 'queued') def _download_next(self) -> None: if self._to_download: video_id = self._to_download.pop(0) with YoutubeDL(YT_DL_PARAMS) as ydl: + self._update_status(video_id, 'downloading') ydl.download([f'{YOUTUBE_URL_PREFIX}{video_id}']) self._sync_db()