sync playlists

This commit is contained in:
jr-k 2024-05-25 19:04:16 +02:00
parent 235d5df5e9
commit 28d9269e36
14 changed files with 81 additions and 12 deletions

View File

@ -64,6 +64,7 @@ jQuery(document).ready(function ($) {
showModal('modal-playlist-edit'); showModal('modal-playlist-edit');
$('.modal-playlist-edit input:visible:eq(0)').focus().select(); $('.modal-playlist-edit input:visible:eq(0)').focus().select();
$('#playlist-edit-name').val(playlist.name); $('#playlist-edit-name').val(playlist.name);
$('#playlist-edit-time-sync').val(playlist.time_sync ? '1' : '0');
$('#playlist-edit-id').val(playlist.id); $('#playlist-edit-id').val(playlist.id);
}); });

View File

@ -4,6 +4,15 @@
#### 🔵 You just want a slideshow manager and you'll deal with screen and browser yourself ? You're in the right place. #### 🔵 You just want a slideshow manager and you'll deal with screen and browser yourself ? You're in the right place.
---
## 📺 Run the player
When you run the browser yourself don't forget to use these flags for chromium browser:
```bash
# chromium or chromium-browser
# replace https://duckduckgo.com with valid playlist url
chromium --disable-features=Translate --ignore-certificate-errors --disable-web-security --disable-restore-session-state --autoplay-policy=no-user-gesture-required --start-maximized --allow-running-insecure-content --remember-cert-error-decisions --noerrdialogs --kiosk --incognito --window-position=0,0 --window-size=1920,1080 --display=:0 https://duckduckgo.com
```
--- ---
## 📡 Run the manager ## 📡 Run the manager

View File

@ -4,11 +4,15 @@
#### 🔴 You want to power RaspberryPi and automatically see your slideshow on a screen connected to it and manage your slideshow ? You're in the right place. #### 🔴 You want to power RaspberryPi and automatically see your slideshow on a screen connected to it and manage your slideshow ? You're in the right place.
---
## 🎛️ Hardware installation ## 🎛️ Hardware installation
1. Download RaspberryPi Imager and setup an sdcard with `Raspberry Pi OS Lite` (🚨without desktop, only `Lite` version!). You'll find it under category `Raspberry PI OS (other)` 1. Download RaspberryPi Imager and setup an sdcard with `Raspberry Pi OS Lite` (🚨without desktop, only `Lite` version!). You'll find it under category `Raspberry PI OS (other)`
2. Log into your RaspberryPi locally or via ssh (by default it's `ssh pi@raspberrypi.local`) 2. Log into your RaspberryPi locally or via ssh (by default it's `ssh pi@raspberrypi.local`)
---
## 📺 Run the player ## 📺 Run the player
Install player autorun by executing following script Install player autorun by executing following script
```bash ```bash

View File

@ -55,6 +55,7 @@
"playlist_form_edit_title": "Edit Slide", "playlist_form_edit_title": "Edit Slide",
"playlist_form_edit_submit": "Save", "playlist_form_edit_submit": "Save",
"playlist_form_label_name": "Name", "playlist_form_label_name": "Name",
"playlist_form_label_time_sync": "Sync slides across players",
"playlist_form_button_cancel": "Cancel", "playlist_form_button_cancel": "Cancel",
"js_playlist_delete_confirmation": "Are you sure?", "js_playlist_delete_confirmation": "Are you sure?",
"playlist_delete_has_slides": "Playlist has slides, please remove them before and retry", "playlist_delete_has_slides": "Playlist has slides, please remove them before and retry",

View File

@ -55,6 +55,7 @@
"playlist_form_edit_title": "Modification d'une liste de lecture", "playlist_form_edit_title": "Modification d'une liste de lecture",
"playlist_form_edit_submit": "Enregistrer", "playlist_form_edit_submit": "Enregistrer",
"playlist_form_label_name": "Nom", "playlist_form_label_name": "Nom",
"playlist_form_label_time_sync": "Synchroniser les slides des lecteurs",
"playlist_form_button_cancel": "Annuler", "playlist_form_button_cancel": "Annuler",
"js_playlist_delete_confirmation": "Êtes-vous sûr ?", "js_playlist_delete_confirmation": "Êtes-vous sûr ?",
"playlist_delete_has_slides": "La liste de lecture contient des sldies, supprimez-les avant et réessayez", "playlist_delete_has_slides": "La liste de lecture contient des sldies, supprimez-les avant et réessayez",

View File

@ -14,6 +14,7 @@ class PlayerController(ObController):
def _get_playlist(self, playlist_id: Optional[int] = 0) -> dict: def _get_playlist(self, playlist_id: Optional[int] = 0) -> dict:
enabled_slides = self._model_store.slide().get_slides(enabled=True, playlist_id=playlist_id) enabled_slides = self._model_store.slide().get_slides(enabled=True, playlist_id=playlist_id)
slides = self._model_store.slide().to_dict(enabled_slides) slides = self._model_store.slide().to_dict(enabled_slides)
playlist = self._model_store.playlist().get(playlist_id)
playlist_loop = [] playlist_loop = []
playlist_cron = [] playlist_cron = []
@ -26,6 +27,8 @@ class PlayerController(ObController):
playlist_loop.append(slide) playlist_loop.append(slide)
playlists = { playlists = {
'playlist_id': playlist.id if playlist else None,
'time_sync': playlist.time_sync if playlist else None,
'loop': playlist_loop, 'loop': playlist_loop,
'cron': playlist_cron, 'cron': playlist_cron,
'hard_refresh_request': self._model_store.variable().get_one_by_name("refresh_player_request").as_int() 'hard_refresh_request': self._model_store.variable().get_one_by_name("refresh_player_request").as_int()

View File

@ -41,6 +41,7 @@ class PlaylistController(ObController):
def playlist_add(self): def playlist_add(self):
playlist = Playlist( playlist = Playlist(
name=request.form['name'], name=request.form['name'],
time_sync=request.form['time_sync'],
) )
self._model_store.playlist().add_form(playlist) self._model_store.playlist().add_form(playlist)
@ -51,6 +52,7 @@ class PlaylistController(ObController):
self._model_store.playlist().update_form( self._model_store.playlist().update_form(
id=request.form['id'], id=request.form['id'],
name=request.form['name'], name=request.form['name'],
time_sync=request.form['time_sync'],
) )
return redirect(url_for('playlist_list')) return redirect(url_for('playlist_list'))

View File

@ -17,6 +17,7 @@ class PlaylistManager(ModelManager):
"name CHAR(255)", "name CHAR(255)",
"slug CHAR(255)", "slug CHAR(255)",
"enabled INTEGER DEFAULT 0", "enabled INTEGER DEFAULT 0",
"time_sync INTEGER DEFAULT 1",
"created_by CHAR(255)", "created_by CHAR(255)",
"updated_by CHAR(255)", "updated_by CHAR(255)",
"created_at INTEGER", "created_at INTEGER",
@ -75,7 +76,7 @@ class PlaylistManager(ModelManager):
if not with_default: if not with_default:
return playlists return playlists
return [Playlist(id=None, name=self.t('slideshow_playlist_panel_item_default'))] + playlists return [Playlist(id=None, time_sync=True, name=self.t('slideshow_playlist_panel_item_default'))] + playlists
def get_disabled_playlists(self) -> List[Playlist]: def get_disabled_playlists(self) -> List[Playlist]:
return self.get_by(query="enabled = 0") return self.get_by(query="enabled = 0")
@ -116,14 +117,15 @@ class PlaylistManager(ModelManager):
def post_delete(self, playlist_id: str) -> str: def post_delete(self, playlist_id: str) -> str:
return playlist_id return playlist_id
def update_form(self, id: int, name: str) -> None: def update_form(self, id: int, name: str, time_sync: bool) -> None:
playlist = self.get(id) playlist = self.get(id)
if not playlist: if not playlist:
return return
form = { form = {
"name": name "name": name,
"time_sync": time_sync
} }
self._db.update_by_id(self.TABLE_NAME, id, self.pre_update(form)) self._db.update_by_id(self.TABLE_NAME, id, self.pre_update(form))

View File

@ -6,11 +6,12 @@ from typing import Optional, Union
class Playlist: class Playlist:
def __init__(self, name: str = 'Untitled', slug: str = 'untitled', id: Optional[int] = None, enabled: bool = False, created_by: Optional[str] = None, updated_by: Optional[str] = None, created_at: Optional[int] = None, updated_at: Optional[int] = None): def __init__(self, name: str = 'Untitled', slug: str = 'untitled', id: Optional[int] = None, enabled: bool = False, time_sync: bool = True, created_by: Optional[str] = None, updated_by: Optional[str] = None, created_at: Optional[int] = None, updated_at: Optional[int] = None):
self._id = id if id else None self._id = id if id else None
self._name = name self._name = name
self._slug = slug self._slug = slug
self._enabled = enabled self._enabled = enabled
self._time_sync = time_sync
self._created_by = created_by if created_by else None self._created_by = created_by if created_by else None
self._updated_by = updated_by if updated_by else None self._updated_by = updated_by if updated_by else None
self._created_at = int(created_at if created_at else time.time()) self._created_at = int(created_at if created_at else time.time())
@ -28,6 +29,14 @@ class Playlist:
def enabled(self, value: bool): def enabled(self, value: bool):
self._enabled = bool(value) self._enabled = bool(value)
@property
def time_sync(self) -> bool:
return bool(self._time_sync)
@time_sync.setter
def time_sync(self, value: bool):
self._time_sync = bool(value)
@property @property
def created_by(self) -> str: def created_by(self) -> str:
return self._created_by return self._created_by
@ -82,6 +91,7 @@ class Playlist:
f"name='{self.name}',\n" \ f"name='{self.name}',\n" \
f"nameslug='{self.slug}',\n" \ f"nameslug='{self.slug}',\n" \
f"enabled='{self.enabled}',\n" \ f"enabled='{self.enabled}',\n" \
f"time_sync='{self.time_sync}',\n" \
f"created_by='{self.created_by}',\n" \ f"created_by='{self.created_by}',\n" \
f"updated_by='{self.updated_by}',\n" \ f"updated_by='{self.updated_by}',\n" \
f"created_at='{self.created_at}',\n" \ f"created_at='{self.created_at}',\n" \
@ -102,6 +112,7 @@ class Playlist:
"name": self.name, "name": self.name,
"slug": self.slug, "slug": self.slug,
"enabled": self.enabled, "enabled": self.enabled,
"time_sync": self.time_sync,
"created_by": self.created_by, "created_by": self.created_by,
"updated_by": self.updated_by, "updated_by": self.updated_by,
"created_at": self.created_at, "created_at": self.created_at,

View File

@ -211,6 +211,8 @@ def slugify(value):
def seconds_to_hhmmss(seconds): def seconds_to_hhmmss(seconds):
if not seconds:
return ""
hours = seconds // 3600 hours = seconds // 3600
minutes = (seconds % 3600) // 60 minutes = (seconds % 3600) // 60
secs = seconds % 60 secs = seconds % 60

View File

@ -43,7 +43,7 @@
var needHardRefresh = null; var needHardRefresh = null;
// Frontend config // Frontend config
var syncedWithTime = false; var syncWithTime = items['time_sync'];
var tickRefreshResolutionMs = 100; var tickRefreshResolutionMs = 100;
// Frontend flag updates // Frontend flag updates
@ -83,7 +83,7 @@
// Functions // Functions
var itemCheck = setInterval(function () { var itemCheck = setInterval(function () {
fetch('player/playlist').then(function(response) { fetch('/player/playlist' + (items['playlist_id'] ? '/use/'+items['playlist_id'] : '')).then(function(response) {
if (response.ok) { if (response.ok) {
return response.json(); return response.json();
} }
@ -131,7 +131,7 @@
return; return;
} }
if (syncedWithTime) { if (syncWithTime) {
return console.warn('You can\'t seek with synced playlists'); return console.warn('You can\'t seek with synced playlists');
} }
@ -203,7 +203,7 @@
if (isPaused()) { if (isPaused()) {
return pauseClockValue; return pauseClockValue;
} }
if (syncedWithTime) { if (syncWithTime) {
clockValue = Date.now(); clockValue = Date.now();
} else { } else {
clockValue += tickRefreshResolutionMs; clockValue += tickRefreshResolutionMs;
@ -235,9 +235,9 @@
var slide = emptySlide ? emptySlide : nextSlide; var slide = emptySlide ? emptySlide : nextSlide;
preloadSlide(slide.attributes['id'].value, item); preloadSlide(slide.attributes['id'].value, item);
if (!hasMoveOnce && syncedWithTime) { if (!hasMoveOnce && syncWithTime) {
if (accumulatedTime + safe_duration(item) - timeInCurrentLoop < 1) { if (accumulatedTime + safe_duration(item) - timeInCurrentLoop < 1) {
// Prevent glitch when syncedWithTime for first init // Prevent glitch when syncWithTime for first init
continue; continue;
} }
} }
@ -266,7 +266,7 @@
play(); play();
} }
if (isPaused() && !syncedWithTime) { if (isPaused() && !syncWithTime) {
return setTimeout(loadingNextSlide, 500); return setTimeout(loadingNextSlide, 500);
} }

View File

@ -2,6 +2,7 @@
<thead> <thead>
<tr> <tr>
<th>{{ l.playlist_panel_th_name }}</th> <th>{{ l.playlist_panel_th_name }}</th>
<th class="tac"><i class="fa fa-compass"></i></th>
<th class="tac">{{ l.playlist_panel_th_enabled }}</th> <th class="tac">{{ l.playlist_panel_th_enabled }}</th>
<th class="tac">{{ l.playlist_panel_th_duration }}</th> <th class="tac">{{ l.playlist_panel_th_duration }}</th>
<th class="tac">{{ l.playlist_panel_th_activity }}</th> <th class="tac">{{ l.playlist_panel_th_activity }}</th>
@ -30,6 +31,13 @@
{{ playlist.name }} {{ playlist.name }}
</div> </div>
</td> </td>
<td class="tac">
{% if playlist.time_sync %}
{% else %}
{% endif %}
</td>
<td class="tac"> <td class="tac">
{% if playlist.id %} {% if playlist.id %}
<label class="pure-material-switch"> <label class="pure-material-switch">
@ -38,7 +46,12 @@
{% endif %} {% endif %}
</td> </td>
<td class="tac"> <td class="tac">
{{ seconds_to_hhmmss(durations[playlist.id]) }} {% set total_duration = seconds_to_hhmmss(durations[playlist.id]) %}
{% if total_duration %}
{{ total_duration }}
{% else %}
{{ l.common_empty }}
{% endif %}
</td> </td>
<td class="actions tac"> <td class="actions tac">
{% if playlist.id %} {% if playlist.id %}

View File

@ -12,6 +12,16 @@
<input name="name" type="text" id="playlist-add-name" required="required" /> <input name="name" type="text" id="playlist-add-name" required="required" />
</div> </div>
</div> </div>
<div class="form-group">
<label for="playlist-add-time-sync">{{ l.playlist_form_label_time_sync }}</label>
<div class="widget">
<select name="time_sync" type="text" id="playlist-add-time-sync" required="required">
<option value="1"></option>
<option value="0"></option>
</select>
</div>
</div>
<div class="actions"> <div class="actions">
<button type="button" class="btn-normal modal-close"> <button type="button" class="btn-normal modal-close">

View File

@ -14,6 +14,16 @@
<input type="text" name="name" id="playlist-edit-name" required="required" /> <input type="text" name="name" id="playlist-edit-name" required="required" />
</div> </div>
</div> </div>
<div class="form-group">
<label for="playlist-edit-time-sync">{{ l.playlist_form_label_time_sync }}</label>
<div class="widget">
<select name="time_sync" type="text" id="playlist-edit-time-sync" required="required">
<option value="1"></option>
<option value="0"></option>
</select>
</div>
</div>
<div class="actions"> <div class="actions">
<button type="button" class="btn-normal modal-close"> <button type="button" class="btn-normal modal-close">