better delegate duration

This commit is contained in:
jr-k 2024-07-22 03:06:31 +02:00
parent 3c31ccf6fe
commit 3ca566dfee
22 changed files with 93 additions and 69 deletions

View File

@ -54,7 +54,7 @@ jQuery(document).ready(function ($) {
const $scheduleStartGroup = $modal.find('.slide-schedule-group');
const $scheduleEndGroup = $modal.find('.slide-schedule-end-group');
const $durationGroup = $modal.find('.slide-duration-group');
const $autoDurationGroup = $modal.find('.slide-auto-duration-group');
const $delegateDurationGroup = $modal.find('.slide-delegate-duration-group');
const $contentGroup = $modal.find('.slide-content-id-group');
const $triggerStart = $scheduleStartGroup.find('.trigger');
@ -62,7 +62,7 @@ jQuery(document).ready(function ($) {
const $targetCronFieldStart = $scheduleStartGroup.find('.target');
const $targetCronFieldEnd = $scheduleEndGroup.find('.target');
const $targetDuration = $durationGroup.find('input');
const $targetAutoDuration = $autoDurationGroup.find('input');
const $targetDelegateDuration = $delegateDurationGroup.find('input')
const $datetimepickerStart = $scheduleStartGroup.find('.datetimepicker');
const $datetimepickerEnd = $scheduleEndGroup.find('.datetimepicker');
@ -101,12 +101,13 @@ jQuery(document).ready(function ($) {
$datetimepickerStart.toggleClass('hidden', !isDatetimeStart);
$datetimepickerEnd.toggleClass('hidden', !isDatetimeEnd);
// $targetAutoDuration
$autoDurationGroup.toggleClass('hidden', (isNotification && isDatetimeEnd) || !isVideo);
$durationGroup.toggleClass('hidden', (isNotification && isDatetimeEnd) || $targetAutoDuration.prop('checked'));
$scheduleEndGroup.toggleClass('hidden', isLoopStart);
$delegateDurationGroup.toggleClass('hidden', (isNotification && isDatetimeEnd) || !isVideo);
$durationGroup.toggleClass('hidden', (isNotification && isDatetimeEnd) || $targetDelegateDuration.prop('checked'));
$durationGroup.find('.widget input').prop('required', $durationGroup.is(':visible'));
$targetDuration.prop('required', $durationGroup.is(':visible'));
$targetDelegateDuration.prop('disabled', !$delegateDurationGroup.is(':visible'));
$scheduleEndGroup.toggleClass('hidden', isLoopStart);
}
function flushValues() {
@ -178,17 +179,7 @@ jQuery(document).ready(function ($) {
$actionShow.removeClass('hidden');
};
$(document).on('change', '.slide-auto-duration', function () {
const $modal = $(this).parents('.modal:eq(0)');
const $durationGroup = $modal.find('.slide-duration-group');
const $durationInput = $durationGroup.find('input');
if ($(this).prop('checked')) {
$durationInput.val(auto_duration_cheatcode);
} else {
$durationInput.val(3);
}
$(document).on('change', '.slide-delegate-duration', function () {
inputSchedulerUpdate();
});
@ -217,7 +208,7 @@ jQuery(document).ready(function ($) {
inputCallbacks();
$modal.find(tclass + '-auto-duration').prop('checked', slide.duration === auto_duration_cheatcode);
$modal.find(tclass + '-delegate-duration').prop('checked', slide.delegate_duration);
$modal.find('input[type=text]:visible:eq(0)').focus().select();
$modal.find(tclass + '-duration').val(slide.duration);

View File

@ -16,7 +16,7 @@
"slideshow_slide_panel_th_content": "Content",
"slideshow_slide_panel_th_duration": "Ends after",
"slideshow_slide_panel_th_duration_unit": "sec",
"slideshow_slide_panel_th_auto_duration_video": "Video's duration",
"slideshow_slide_panel_th_delegate_duration_video": "Video's duration",
"slideshow_slide_panel_th_enabled": "Enabled",
"slideshow_slide_panel_th_cron_scheduled": "Scheduled Start",
"slideshow_slide_panel_th_activity": "Options",
@ -34,7 +34,7 @@
"slideshow_slide_form_section_scheduling": "Scheduling",
"slideshow_slide_form_label_name": "Name",
"slideshow_slide_form_label_enabled": "Enable/Disable",
"slideshow_slide_form_label_auto_duration": "Use video's duration as timer",
"slideshow_slide_form_label_delegate_duration": "Use video's duration as timer",
"slideshow_slide_form_label_add_content": "Upload to library",
"slideshow_slide_form_label_from_library": "From library",
"slideshow_slide_form_label_content_id": "Content",

View File

@ -16,7 +16,7 @@
"slideshow_slide_panel_th_content": "Contenido",
"slideshow_slide_panel_th_duration": "Termina después",
"slideshow_slide_panel_th_duration_unit": "seg",
"slideshow_slide_panel_th_auto_duration_video": "Duración del vídeo",
"slideshow_slide_panel_th_delegate_duration_video": "Duración del vídeo",
"slideshow_slide_panel_th_enabled": "Habilitado",
"slideshow_slide_panel_th_cron_scheduled": "Inicio Programado",
"slideshow_slide_panel_th_activity": "Opciones",
@ -35,7 +35,7 @@
"slideshow_slide_form_label_name": "Nombre",
"slideshow_slide_form_label_add_content": "Upload a la biblioteca",
"slideshow_slide_form_label_enabled": "Activar/Desactivar",
"slideshow_slide_form_label_auto_duration": "Utilizar la duración del vídeo",
"slideshow_slide_form_label_delegate_duration": "Utilizar la duración del vídeo",
"slideshow_slide_form_label_from_library": "Dalla biblioteca",
"slideshow_slide_form_label_content_id": "Contenido",
"slideshow_slide_form_label_location": "Ubicación",

View File

@ -16,7 +16,7 @@
"slideshow_slide_panel_th_content": "Contenu",
"slideshow_slide_panel_th_duration": "Fin après",
"slideshow_slide_panel_th_duration_unit": "sec",
"slideshow_slide_panel_th_auto_duration_video": "Durée de la vidéo",
"slideshow_slide_panel_th_delegate_duration_video": "Durée de la vidéo",
"slideshow_slide_panel_th_enabled": "Activé",
"slideshow_slide_panel_th_cron_scheduled": "Programmation",
"slideshow_slide_panel_th_activity": "Options",
@ -34,7 +34,7 @@
"slideshow_slide_form_section_scheduling": "Programmation",
"slideshow_slide_form_label_name": "Nom",
"slideshow_slide_form_label_enabled": "Activer/Désactiver",
"slideshow_slide_form_label_auto_duration": "Utiliser la durée de la vidéo",
"slideshow_slide_form_label_delegate_duration": "Utiliser la durée de la vidéo",
"slideshow_slide_form_label_add_content": "Upload à la bibliothèque",
"slideshow_slide_form_label_from_library": "Depuis la bibliothèque",
"slideshow_slide_form_label_content_id": "Contenu",

View File

@ -16,7 +16,7 @@
"slideshow_slide_panel_th_content": "Contenuti",
"slideshow_slide_panel_th_duration": "Finito in",
"slideshow_slide_panel_th_duration_unit": "sec",
"slideshow_slide_panel_th_auto_duration_video": "Durata del video",
"slideshow_slide_panel_th_delegate_duration_video": "Durata del video",
"slideshow_slide_panel_th_enabled": "Abilitato",
"slideshow_slide_panel_th_cron_scheduled": "Avvia programmazione",
"slideshow_slide_panel_th_activity": "Opzioni",
@ -34,7 +34,7 @@
"slideshow_slide_form_section_scheduling": "Programmazione",
"slideshow_slide_form_label_name": "Nome",
"slideshow_slide_form_label_enabled": "Abilita/Disabilita",
"slideshow_slide_form_label_auto_duration": "Utilizza la durata del video",
"slideshow_slide_form_label_delegate_duration": "Utilizza la durata del video",
"slideshow_slide_form_label_add_content": "Upload alla biblioteca",
"slideshow_slide_form_label_from_library": "Dalla biblioteca",
"slideshow_slide_form_label_content_id": "Contenuti",

View File

@ -5,3 +5,4 @@ waitress
flask-login
pysqlite3
psutil
moviepy

View File

@ -8,7 +8,7 @@ from flask import Flask, render_template, redirect, request, url_for, send_from_
from pathlib import Path
from src.model.entity.Slide import Slide
from src.model.enum.ContentType import ContentType, AUTO_DURATION_CHEATCODE
from src.model.enum.ContentType import ContentType
from src.exceptions.NoFallbackPlaylistException import NoFallbackPlaylistException
from src.service.ModelStore import ModelStore
from src.interface.ObController import ObController
@ -61,7 +61,6 @@ class PlayerController(ObController):
slide_animation_entrance_effect=slide_animation_entrance_effect,
slide_animation_speed=slide_animation_speed,
animation_speed_duration=animation_speed_duration,
auto_duration_cheatcode=AUTO_DURATION_CHEATCODE
)
def player_default(self):

View File

@ -4,7 +4,7 @@ from flask import Flask, render_template, redirect, request, url_for, jsonify, a
from src.service.ModelStore import ModelStore
from src.model.entity.Playlist import Playlist
from src.model.enum.FolderEntity import FolderEntity
from src.model.enum.ContentType import ContentType, AUTO_DURATION_CHEATCODE
from src.model.enum.ContentType import ContentType
from src.interface.ObController import ObController
@ -49,7 +49,6 @@ class PlaylistController(ObController):
folders_tree=self._model_store.folder().get_folder_tree(FolderEntity.CONTENT),
enum_content_type=ContentType,
enum_folder_entity=FolderEntity,
auto_duration_cheatcode=AUTO_DURATION_CHEATCODE,
)
def playlist_add(self):

View File

@ -62,7 +62,8 @@ class SlideController(ObController):
slide = self._model_store.slide().update_form(
id=request.form['id'],
content_id=request.form['content_id'],
enabled='enabled' in request.form and request.form['enabled'],
enabled='enabled' in request.form and request.form['enabled'] == '1',
delegate_duration='delegate_duration' in request.form and request.form['delegate_duration'] == '1',
duration=request.form['duration'],
is_notification=True if 'is_notification' in request.form and request.form['is_notification'] == '1' else False,
cron_schedule=request.form['cron_schedule'],

View File

@ -2,6 +2,7 @@ import os
from typing import Dict, Optional, List, Tuple, Union
from werkzeug.datastructures import FileStorage
from moviepy.editor import VideoFileClip
from src.model.entity.Content import Content
from src.model.entity.Playlist import Playlist
@ -25,6 +26,7 @@ class ContentManager(ModelManager):
"name CHAR(255)",
"type CHAR(30)",
"location TEXT",
"duration INTEGER",
"folder_id INTEGER",
"created_by CHAR(255)",
"updated_by CHAR(255)",
@ -125,7 +127,7 @@ class ContentManager(ModelManager):
def post_delete(self, content_id: str) -> str:
return content_id
def update_form(self, id: int, name: str, location: Optional[str] = None) -> Content:
def update_form(self, id: int, name: str, location: Optional[str] = None) -> Optional[Content]:
content = self.get(id)
if not content:
@ -191,6 +193,11 @@ class ContentManager(ModelManager):
object_path = os.path.join(upload_dir, object_name)
object.save(object_path)
content.location = object_path
if type == ContentType.VIDEO:
with VideoFileClip(content.location) as video:
content.duration = int(video.duration)
else:
content.location = location

View File

@ -153,7 +153,7 @@ class NodePlayerGroupManager(ModelManager):
form = {
"name": name,
"playlist_id": playlist_id if playlist_id else None
"playlist_id": playlist_id if playlist_id else node_player_group.playlist_id
}
self._db.update_by_id(self.TABLE_NAME, id, self.pre_update(form))

View File

@ -6,6 +6,7 @@ from src.model.entity.Playlist import Playlist
from src.util.utils import get_optional_string, get_yt_video_id, slugify, slugify_next
from src.manager.DatabaseManager import DatabaseManager
from src.manager.SlideManager import SlideManager
from src.manager.ContentManager import ContentManager
from src.manager.LangManager import LangManager
from src.manager.UserManager import UserManager
from src.manager.VariableManager import VariableManager
@ -65,8 +66,20 @@ class PlaylistManager(ModelManager):
return self.hydrate_object(object)
def get_durations_by_playlists(self, playlist_id: Optional[int] = None):
durations = self._db.execute_read_query("select playlist_id, sum(duration) as total_duration from {} where cron_schedule is null {} group by playlist_id".format(
durations = self._db.execute_read_query("""
SELECT
playlist_id,
SUM(CASE
WHEN s.delegate_duration = 1 THEN c.duration
ELSE s.duration
END) AS total_duration
FROM {} s
LEFT JOIN {} c ON c.id = s.content_id
WHERE cron_schedule IS NULL {}
GROUP BY playlist_id;
""".format(
SlideManager.TABLE_NAME,
ContentManager.TABLE_NAME,
"{}".format(
" AND playlist_id = {}".format(playlist_id) if playlist_id else ""
)
@ -145,7 +158,7 @@ class PlaylistManager(ModelManager):
def post_delete(self, playlist_id: str) -> str:
return playlist_id
def update_form(self, id: int, name: str, time_sync: bool, enabled: bool) -> None:
def update_form(self, id: int, name: Optional[str] = None, time_sync: Optional[bool] = None, enabled: Optional[bool] = None) -> None:
playlist = self.get(id)
if not playlist:
@ -153,8 +166,8 @@ class PlaylistManager(ModelManager):
form = {
"name": name,
"time_sync": time_sync,
"enabled": enabled
"time_sync": time_sync if isinstance(time_sync, bool) else slide.time_sync,
"enabled": enabled if isinstance(enabled, bool) else slide.enabled,
}
self._db.update_by_id(self.TABLE_NAME, id, self.pre_update(form))

View File

@ -17,6 +17,7 @@ class SlideManager(ModelManager):
TABLE_NAME = "slides"
TABLE_MODEL = [
"enabled INTEGER DEFAULT 0",
"delegate_duration INTEGER DEFAULT 0",
"is_notification INTEGER DEFAULT 0",
"playlist_id INTEGER",
"content_id INTEGER",
@ -135,16 +136,17 @@ class SlideManager(ModelManager):
for slide_id, slide_position in positions.items():
self._db.update_by_id(self.TABLE_NAME, slide_id, {"position": slide_position})
def update_form(self, id: int, duration: int, content_id: Optional[int] = None, is_notification: bool = False, cron_schedule: Optional[str] = '', cron_schedule_end: Optional[str] = '', enabled: bool = True) -> Slide:
def update_form(self, id: int, duration: Optional[int] = None, content_id: Optional[int] = None, delegate_duration: Optional[bool] = None, is_notification: bool = False, cron_schedule: Optional[str] = '', cron_schedule_end: Optional[str] = '', enabled: Optional[bool] = None) -> Optional[Slide]:
slide = self.get(id)
if not slide:
return
form = {
"duration": duration,
"content_id": content_id,
"enabled": enabled,
"duration": duration if duration else slide.duration,
"content_id": content_id if content_id else slide.content_id,
"enabled": enabled if isinstance(enabled, bool) else slide.enabled,
"delegate_duration": delegate_duration if isinstance(delegate_duration, bool) else slide.delegate_duration,
"is_notification": True if is_notification else False,
"cron_schedule": get_optional_string(cron_schedule),
"cron_schedule_end": get_optional_string(cron_schedule_end)

View File

@ -164,7 +164,7 @@ class UserManager:
form = {
"username": username if user else user.username,
"enabled": enabled if enabled is not None else user.enabled
"enabled": enabled if isinstance(enabled, bool) else user.enabled
}
if password is not None and password:

View File

@ -9,13 +9,14 @@ from src.util.utils import str_to_enum
class Content:
def __init__(self, uuid: str = '', location: str = '', type: Union[ContentType, str] = ContentType.URL, name: str = 'Untitled', id: Optional[int] = None, created_by: Optional[str] = None, updated_by: Optional[str] = None, created_at: Optional[int] = None, updated_at: Optional[int] = None, folder_id: Optional[int] = None):
def __init__(self, uuid: str = '', location: str = '', type: Union[ContentType, str] = ContentType.URL, name: str = 'Untitled', id: Optional[int] = None, duration: Optional[int] = None, created_by: Optional[str] = None, updated_by: Optional[str] = None, created_at: Optional[int] = None, updated_at: Optional[int] = None, folder_id: Optional[int] = None):
self._uuid = uuid if uuid else self.generate_and_set_uuid()
self._id = id if id else None
self._location = location
self._type = str_to_enum(type, ContentType) if isinstance(type, str) else type
self._name = name
self._folder_id = folder_id
self._duration = duration
self._created_by = created_by if created_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())
@ -86,6 +87,14 @@ class Content:
def folder_id(self, value: Optional[int]):
self._folder_id = value
@property
def duration(self) -> Optional[int]:
return self._duration
@duration.setter
def duration(self, value: Optional[int]):
self._duration = value
@property
def type(self) -> ContentType:
return self._type
@ -114,6 +123,7 @@ class Content:
f"created_at='{self.created_at}',\n" \
f"updated_at='{self.updated_at}',\n" \
f"folder_id='{self.folder_id}',\n" \
f"duration='{self.duration}',\n" \
f")"
def to_json(self, edits: dict = {}) -> str:
@ -136,6 +146,7 @@ class Content:
"created_at": self.created_at,
"updated_at": self.updated_at,
"folder_id": self.folder_id,
"duration": self.duration,
}
if with_virtual:

View File

@ -3,16 +3,16 @@ import time
from typing import Optional, Union
from src.util.utils import str_to_enum
from src.model.enum.ContentType import AUTO_DURATION_CHEATCODE
class Slide:
def __init__(self, playlist_id: Optional[int] = None, content_id: Optional[int] = None, duration: int = 3, is_notification: bool = False, enabled: bool = False, position: int = 999, id: Optional[int] = None, cron_schedule: Optional[str] = None, cron_schedule_end: Optional[str] = None, created_by: Optional[str] = None, updated_by: Optional[str] = None, created_at: Optional[int] = None, updated_at: Optional[int] = None):
def __init__(self, playlist_id: Optional[int] = None, content_id: Optional[int] = None, delegate_duration=False, duration: int = 3, is_notification: bool = False, enabled: bool = False, position: int = 999, id: Optional[int] = None, cron_schedule: Optional[str] = None, cron_schedule_end: Optional[str] = None, 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._playlist_id = playlist_id
self._content_id = content_id
self._duration = duration
self._delegate_duration = delegate_duration
self._enabled = enabled
self._is_notification = is_notification
self._position = position
@ -91,6 +91,14 @@ class Slide:
def cron_schedule_end(self, value: Optional[str]):
self._cron_schedule_end = value
@property
def delegate_duration(self) -> bool:
return self._delegate_duration
@delegate_duration.setter
def delegate_duration(self, value: bool):
self._delegate_duration = value
@property
def duration(self) -> int:
return self._duration
@ -129,6 +137,7 @@ class Slide:
f"enabled='{self.enabled}',\n" \
f"is_notification='{self.is_notification}',\n" \
f"duration='{self.duration}',\n" \
f"delegate_duration='{self.delegate_duration}',\n" \
f"position='{self.position}',\n" \
f"created_by='{self.created_by}',\n" \
f"updated_by='{self.updated_by}',\n" \
@ -155,6 +164,7 @@ class Slide:
"is_notification": self.is_notification,
"position": self.position,
"duration": self.duration,
"delegate_duration": self.delegate_duration,
"created_by": self.created_by,
"updated_by": self.updated_by,
"created_at": self.created_at,
@ -166,6 +176,3 @@ class Slide:
}
return slide
def has_auto_duration(self) -> bool:
return self.duration == AUTO_DURATION_CHEATCODE

View File

@ -5,8 +5,6 @@ from typing import Union, List, Optional
from src.util.utils import str_to_enum
AUTO_DURATION_CHEATCODE = 98769876
class ContentInputType(Enum):
@ -26,11 +24,11 @@ class ContentInputType(Enum):
class ContentType(Enum):
EXTERNAL_STORAGE = 'external_storage'
PICTURE = 'picture'
URL = 'url'
YOUTUBE = 'youtube'
VIDEO = 'video'
EXTERNAL_STORAGE = 'external_storage'
@staticmethod
def guess_content_type_file(filename: str):

View File

@ -158,10 +158,6 @@
pause();
};
const isDurationAuto = function(duration) {
return duration === {{ auto_duration_cheatcode }};
} ;
const lookupPreviousItem = function() {
return (curItemIndex - 1 < 0) ? items.loop[items.loop.length - 1] : items.loop[curItemIndex - 1];
};
@ -245,7 +241,7 @@
if (i === curItemIndex) {
secondsBeforeNext = accumulatedTime + safe_duration(item) - timeInCurrentLoop;
//console.log("remaining:", secondsBeforeNext, "clock:",clockValue, curItemIndex);
console.log("remaining:", secondsBeforeNext, "clock:",clockValue, curItemIndex);
}
if (timeInCurrentLoop < accumulatedTime + safe_duration(item)) {
@ -375,7 +371,7 @@
delayNoisyContentJIT = lookupCurrentItem().id !== item.id ? delayNoisyContentJIT : 0;
video.addEventListener('loadedmetadata', function() {
if (item.duration !== video.duration && isDurationAuto(item.duration)) {
if (item.duration !== video.duration && item.delegate_duration) {
durationsOverride[item.id] = video.duration;
console.warn('Given duration ' + item.duration + 's is different from video file ' + Math.ceil(video.duration) + 's');
}

View File

@ -13,7 +13,6 @@
{% block add_js %}
<script type="text/javascript">
var auto_duration_cheatcode = {{ auto_duration_cheatcode }};
var route_slide_position = '{{ url_for('slideshow_slide_position') }}';
var choices_translations = {

View File

@ -63,8 +63,8 @@
<span class="error"><i class="fa fa-warning danger"></i> {{ l.slideshow_slide_panel_td_cron_scheduled_bad_cron }}</span>
{% endif %}
{% else %}
{% if slide.has_auto_duration() %}
<i class="fa {{ icon }}"></i> <span class="prefix">{{ l.slideshow_slide_panel_th_auto_duration_video }}</span>
{% if slide.delegate_duration %}
<i class="fa {{ icon }}"></i> <span class="prefix">{{ l.slideshow_slide_panel_th_delegate_duration_video }}</span>
{% else %}
⏱️ <span class="prefix">{{ slide.duration }} {{ l.slideshow_slide_panel_th_duration_unit }}</span>
{% endif %}

View File

@ -74,12 +74,12 @@
</div>
</div>
<div class="form-group form-group-horizontal slide-auto-duration-group">
<label for="">{{ l.slideshow_slide_form_label_auto_duration }}</label>
<div class="form-group form-group-horizontal slide-delegate-duration-group">
<label for="">{{ l.slideshow_slide_form_label_delegate_duration }}</label>
<div class="widget">
<div class="toggle">
<input type="checkbox" class="slide-auto-duration" id="{{ tclass }}-auto-duration" />
<label for="{{ tclass }}-auto-duration"></label>
<input type="checkbox" name="delegate_duration" class="slide-delegate-duration" id="{{ tclass }}-delegate-duration" value="1" disabled />
<label for="{{ tclass }}-delegate-duration"></label>
</div>
</div>
</div>

View File

@ -72,12 +72,12 @@
</div>
</div>
<div class="form-group form-group-horizontal slide-auto-duration-group">
<label for="">{{ l.slideshow_slide_form_label_auto_duration }}</label>
<div class="form-group form-group-horizontal slide-delegate-duration-group">
<label for="">{{ l.slideshow_slide_form_label_delegate_duration }}</label>
<div class="widget">
<div class="toggle">
<input type="checkbox" class="slide-auto-duration" id="{{ tclass }}-auto-duration" />
<label for="{{ tclass }}-auto-duration"></label>
<input type="checkbox" name="delegate_duration" class="slide-delegate-duration" id="{{ tclass }}-delegate-duration" value="1" disabled />
<label for="{{ tclass }}-delegate-duration"></label>
</div>
</div>
</div>