{% 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 %}
<table>
<tr><th>title:</th><td>{{video_data.title}}</td></tr>
<tr><th>duration:</th><td>{{video_data.duration}}</td></tr>
<tr><th>definition:</th><td>{{video_data.definition}}</td></tr>
<tr><th>YouTube ID:</th><td>{{video_data.id_}}</td></tr>
-<tr><th>download:</th><td>{% if is_temp %}working on it{% elif file_path %}<a href="/{{page_names.file}}/{{file_digest}}">{{file_path}}</a>{% else %}<a href="/{{page_names.download}}/{{video_data.id_}}">please do</a>{% endif %}</td></tr>
+<tr><th>download:</th><td id="status"></td></tr>
<tr>
<th>linked queries:</th>
<td>
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:
('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'] = {}
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': [
'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:
"""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
@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,
"""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()
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):
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:
+ [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()