<style>
body { background-color: #aaaa00; }
table { width: 100%; }
-th { text-align: left; }
-td { vertical-align: top; }
+td, th { vertical-align: top; text-align: left; }
{% block css %}
{% endblock %}
</style>
</p>
<hr />
{% endmacro %}
+
+
+{% macro file_data_form(file, unused_tags, page_names, flag_names=[], playlist_view=false) %}
+<form action="/{{page_names.file}}/{{file.digest.b64}}" method="POST" />
+<input type="hidden" name="redir" value="/{% if playlist_view %}{{page_names.playlist}}{% else %}{{page_names.file}}/{{file.digest.b64}}{% endif %}" />
+<table>
+<tr><th>path:</th><td class="top_field">{% if playlist_view %}<a href="/{{page_names.file}}/{{file.digest.b64}}">{% endif %}{{file.rel_path}}{% if playlist_view %}</a>{% endif %}</td></tr>
+{% if not playlist_view %}
+<tr><th>present:</th><td>{% if file.present %}<a href="/{{page_names.download}}/{{file.yt_id}}">yes</a>{% else %}no{% endif %}</td></tr>
+{% endif %}
+<tr><th>YouTube ID:</th><td><a href="/{{page_names.yt_result}}/{{file.yt_id}}">{{file.yt_id}}</a></tr>
+<tr>
+<th>tags</th>
+<td>
+<table>
+{% for tag in file.tags %}
+<tr><td class="tag_checkboxes"><input type="checkbox" name="tags" value="{{tag}}" checked /></td><td>{{tag}}</td></tr>
+{% endfor %}
+<tr><td class="tag_checkboxes">add:</td><td><input name="tags" list="unused_tags" autocomplete="off" /></td></tr>
+<datalist id="unused_tags" />
+{% for tag in unused_tags %}
+<option value="{{tag}}">{{tag}}</option>
+{% endfor %}
+</datalist>
+</table>
+</td>
+</tr>
+{% if not playlist_view %}
+<tr>
+<th>flags:</th>
+<td class="flags">
+{% for flag_name in flag_names %}
+{{ flag_name }}: <input type="checkbox" name="flags" value="{{flag_name}}" {% if file.is_flag_set(flag_name) %}checked {% endif %} /><br />
+{% endfor %}
+</td>
+</tr>
+{% endif %}
+</table>
+<input type="submit" />
+</form>
+{% endmacro %}
{% block css %}
-td { width: 100%; }
+td.top_field { width: 100%; }
td.flags { text-align: right; }
+td.tag_checkboxes { width: 1em; }
{% endblock %}
{% block body %}
{{ macros.nav_head(page_names) }}
-<form action="/{{page_names.file}}/{{file.digest.b64}}" method="POST" />
-<table>
-<tr><th>path:</th><td>{{file.rel_path}}</td></tr>
-<tr><th>YouTube ID:</th><td><a href="/{{page_names.yt_result}}/{{file.yt_id}}">{{file.yt_id}}</a></tr>
-<tr><th>present:</th><td>{% if file.present %}<a href="/{{page_names.download}}/{{file.yt_id}}">yes</a>{% else %}no{% endif %}</td></tr>
-<tr>
-<th>flags:</th>
-<td class="flags">
-{% for flag_name in flag_names %}
-{{ flag_name }}: <input type="checkbox" name="flags" value="{{flag_name}}" {% if file.is_flag_set(flag_name) %}checked {% endif %} /><br />
-{% endfor %}
-</td>
-</tr>
-<tr>
-<th>tags</th>
-<td>
-{% for tag in file.tags %}
-<input type="checkbox" name="tags" value="{{tag}}" checked /> {{tag}}<br />
-{% endfor %}
-<input name="tags" list="unused_tags" autocomplete="off" />
-<datalist id="unused_tags" />
-{% for tag in unused_tags %}
-<option value="{{tag}}">{{tag}}</option>
-{% endfor %}
-</datalist>
-</td>
-</tr>
-</table>
-<input type="submit" />
-</form>
+{{ macros.file_data_form(file, unused_tags, page_names, flag_names) }}
{% endblock %}
<tr><th>size</th><th>actions</th><th>tags</th><th>path</th></tr>
{% for file in files %}
<tr>
-<td>{{ file.size | round(3) }}</td>
+<td>{{ file.size | round(1) }}</td>
<td><input type="submit" name="play_{{file.digest.b64}}" value="play" {% if not file.present %}disabled {% endif %}/></td>
<td>{% for tag in file.tags %}{{tag}} {%endfor %}</td>
<td><a href="/{{page_names.file}}/{{file.digest.b64}}">{{file.rel_path}}</a></td>
{% block css %}
-#status { text-align: center; font-weight: bold; }
-th { text-align: center; }
-td.history { width: 50%; }
+#status { font-weight: bold; }
+td.screen_half { width: 50%; }
+tr.screen_half_titles>th { text-align: center; }
td.entry_buttons { width: 5em; }
+td.tag_checkboxes { width: 1em; }
{% endblock %}
-{% macro playlist_entries(files_w_idx, reverse) %}
-<td class="history">
-<table>
-{% for idx, file in files_w_idx %}
-<tr>
-<td class="entry_buttons">
-<input type="submit" name="jump_{{idx}}" value=">" />
-<input type="submit" name="up_{{idx}}" value="{% if reverse %}v{% else %}^{% endif %}" />
-<input type="submit" name="down_{{idx}}" value="{% if reverse %}^{% else %}v{% endif %}" />
-</td>
-<td><a href="/{{page_names.file}}/{{file.digest.b64}}">{{ file.rel_path }}</a></td>
-</tr>
-{% endfor %}
-</table>
-</td>
-{% endmacro %}
-
-
{% block body %}
{{ macros.nav_head(page_names, "playlist") }}
<table>
<tr><td id="status" colspan=2>
-{% if running %}{% if pause %}PAUSED{% else %}PLAYING{% endif %}{% else %}STOPPED{% endif %}:<br />
-<a href="/{{page_names.file}}/{{current_video.digest.b64}}">{{ current_video.rel_path }}</a><br />
<form action="/{{page_names.playlist}}" method="POST">
<input type="submit" name="pause" autofocus value="{% if paused %}resume{% else %}pause{% endif %}">
<input type="submit" name="prev" value="prev">
<input type="submit" name="next" value="next">
<input type="submit" name="stop" value="{% if running %}stop{% else %}start{% endif %}">
<input type="submit" name="reload" value="reload">
+{% if running %}{% if pause %}PAUSED{% else %}PLAYING{% endif %}{% else %}STOPPED{% endif %}
+</form>
</td></tr>
-<tr><th>past</th><th>future</th></tr>
+<tr class="screen_half_titles"><th>current selection</th><th>playlist</th></tr>
<tr>
-{{ playlist_entries(prev_files_w_idx, reverse=true) }}
-{{ playlist_entries(next_files_w_idx, reverse=false) }}
+<td class="screen_half">
+{{ macros.file_data_form(current_file, unused_tags, page_names, playlist_view=true) }}
+</td>
+<td class="screen_half">
+<form action="/{{page_names.playlist}}" method="POST">
+<table>
+{% for idx, file in files_w_idx %}
+<tr>
+<td class="entry_buttons">
+{% if file.digest == current_file.digest %}
+PLAYING
+{% else %}
+<input type="submit" name="jump_{{idx}}" value=">" />
+<input type="submit" name="up_{{idx}}" value="{% if reverse %}v{% else %}^{% endif %}" />
+<input type="submit" name="down_{{idx}}" value="{% if reverse %}^{% else %}v{% endif %}" />
+{% endif %}
+</td>
+<td><a href="/{{page_names.file}}/{{file.digest.b64}}">{{ file.rel_path }}</a></td>
</tr>
+{% endfor %}
+</table>
</form>
+</td>
+</tr>
</table>
{% endblock %}
None | bool
| FilesWithIndex | _PageNames | _FilterStr | Path | PlayerUpdateId
| QueryText | QuotaCost | UrlStr | 'VideoFile' | YoutubeId
- | 'YoutubeVideo' | list[FlagName] | list['Tag'] | list['VideoFile']
+ | 'YoutubeVideo' | list[FlagName] | set['Tag'] | list['VideoFile']
| list['YoutubeVideo'] | list['YoutubeQuery']
]
file.save(conn)
conn.commit()
file.ensure_absence_if_deleted()
- self._redirect(Path('/')
- .joinpath(PAGE_NAMES['file'])
- .joinpath(digest.b64))
+ self._redirect(Path(postvars['redir'][0]))
def _receive_yt_query(self, query_txt: QueryText) -> None:
with DbConn() as conn:
def _send_file_data(self, digest: Hash) -> None:
with DbConn() as conn:
file = VideoFile.get_one(conn, digest)
- all_tags = VideoFile.get_all_tags(conn)
- self._send_rendered_template(
- _NAME_TEMPLATE_FILE_DATA,
- {'file': file, 'flag_names': list(FILE_FLAGS),
- 'unused_tags': [t for t in all_tags if t not in file.tags]})
+ unused_tags = file.unused_tags(conn)
+ self._send_rendered_template(_NAME_TEMPLATE_FILE_DATA,
+ {'file': file,
+ 'flag_names': list(FILE_FLAGS),
+ 'unused_tags': unused_tags})
def _send_files_index(self, params: dict[str, list[str]]) -> None:
filter_path = _FilterStr(params.get('filter_path', [''])[0])
def _send_playlist(self) -> None:
if self.server.player.empty:
self.server.player.load_files()
+ current_file, unused_tags = None, set()
+ if self.server.player.current_file_digest:
+ with DbConn() as conn:
+ current_file = VideoFile.get_one(
+ conn, self.server.player.current_file_digest)
+ unused_tags = current_file.unused_tags(conn)
self._send_rendered_template(
_NAME_TEMPLATE_PLAYLIST,
{'last_update': self.server.player.last_update,
'running': self.server.player.is_running,
'paused': self.server.player.is_paused,
- 'current_video': self.server.player.current_file,
- 'prev_files_w_idx': self.server.player.prev_files_w_idx,
- 'next_files_w_idx': self.server.player.next_files_w_idx})
+ 'current_file': current_file,
+ 'unused_tags': unused_tags,
+ 'files_w_idx': list(enumerate(self.server.player.files))
+ })
f'{self.yt_id}|{self.last_update}|{self.tags_str}')
def _renew_last_update(self):
- print("DEBUG calling_renew_last_update", self.rel_path)
self.last_update = DatetimeStr(datetime.now().strftime(TIMESTAMP_FMT))
self._hash_on_last_update = hash(self)
raise NotFoundException(f'no entry for file to Youtube ID {yt_id}')
return cls._from_table_row(row)
- @classmethod
- def get_all_tags(cls, conn: BaseDbConn) -> set[Tag]:
- """Return all tags used among VideoFiles."""
+ def unused_tags(self, conn: BaseDbConn) -> set[Tag]:
+ """Return tags used among other VideoFiles, not in self."""
tags = set()
- for file in cls.get_all(conn):
- for tag in file.tags:
+ for file in self.get_all(conn):
+ for tag in [t for t in file.tags if t not in self.tags]:
tags.add(tag)
return tags
"""Collect files in PATH_DOWNLOADS DB-known and of legal extension."""
with DbConn() as conn:
known_files = {f.full_path: f for f in VideoFile.get_all(conn)}
- self._files = [known_files[p] for p in PATH_DOWNLOADS.iterdir()
- if p in known_files
- and p.is_file()
- and p.suffix[1:] in LEGAL_EXTENSIONS]
- shuffle(self._files)
+ self.files = [known_files[p] for p in PATH_DOWNLOADS.iterdir()
+ if p in known_files
+ and p.is_file()
+ and p.suffix[1:] in LEGAL_EXTENSIONS]
+ shuffle(self.files)
self._idx = 0
def _signal_update(self) -> None:
config=True)
self._monitor_kill()
self._mpv.observe_property('pause', lambda a, b: self._signal_update())
- for path in [f.full_path for f in self._files]:
+ for path in [f.full_path for f in self.files]:
self._mpv.command('loadfile', path, 'append')
@self._mpv.event_callback('start-file')
@property
def empty(self) -> bool:
"""Return if playlist empty."""
- return 0 == len(self._files)
+ return 0 == len(self.files)
@property
- def current_file(self) -> Optional[VideoFile]:
- """Return what we assume is the currently playing file."""
- if not self._files:
- return None
- return self._files[self._idx]
+ def current_file_digest(self) -> Optional[Hash]:
+ """Return .digest of what we assume is the currently playing file.
- @property
- def _files_w_idx(self) -> FilesWithIndex:
- return list(enumerate(self._files))
-
- @property
- def prev_files_w_idx(self) -> FilesWithIndex:
- """List 'past' files of playlist."""
- return list(reversed(self._files_w_idx[:self._idx]))
-
- @property
- def next_files_w_idx(self) -> FilesWithIndex:
- """List 'coming' files of playlist."""
- return self._files_w_idx[self._idx + 1:]
+ We don't return the actual file object because we cannot guarantee its
+ data's up-to-date-ness, it being cached from the last .load_files call.
+ """
+ if not self.files:
+ return None
+ return self.files[self._idx].digest
@property
def is_running(self) -> bool:
def next(self) -> None:
"""Move player to next item in playlist."""
- if self._idx < len(self._files) - 1:
+ if self._idx < len(self.files) - 1:
self._idx += 1
self._play_at_index()
or (upwards and start_idx == self._idx + 1)
or ((not upwards) and start_idx == self._idx - 1)
or (upwards and start_idx < 1)
- or ((not upwards) and start_idx > len(self._files) - 2)):
+ or ((not upwards) and start_idx > len(self.files) - 2)):
return
i0, i1 = start_idx, start_idx + (-1 if upwards else 1)
if self._mpv:
# NB: a functional playlist-move would do this in a single step,
# but for some reason I don't seem to get it to do anything
- path = self._files[i1].full_path
+ path = self.files[i1].full_path
self._mpv.command('playlist-remove', i1)
self._mpv.command('loadfile', path, 'insert-at', i0)
- self._files[i0], self._files[i1] = self._files[i1], self._files[i0]
+ self.files[i0], self.files[i1] = self.files[i1], self.files[i0]
def reload(self) -> None:
"""Close MPV, re-read (and re-shuffle) filenames, then re-start MPV."""
def inject_and_play(self, file: VideoFile) -> None:
"""Inject file after current title, then jump to it."""
- if self._files:
+ if self.files:
self._idx += 1
- self._files.insert(self._idx, file)
+ self.files.insert(self._idx, file)
if self._mpv:
self._mpv.command('loadfile', file.full_path,
'insert-at', self._idx)