Merge pull request #131 from jr-k/develop

Release v2.3.1
This commit is contained in:
JRK 2024-08-06 12:23:15 +02:00 committed by GitHub
commit 9e54506dcc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 287 additions and 32 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -41,5 +41,61 @@ jQuery(document).ready(function ($) {
} }
}); });
$(document).on('click', '.cast-scan', function () {
showModal('modal-playlist-cast-scan');
const $modal = $('.modal-playlist-cast-scan:visible');
const $holder = $modal.find('.cast-devices');
const $loading = $modal.find('.loading');
$loading.removeClass('hidden');
$holder.removeClass('hidden');
$holder.html('');
$loading.html($loading.attr('data-loading'));
$.ajax({
method: 'GET',
url: route_cast_scan,
headers: {'Content-Type': 'application/json'},
success: function (response) {
$loading.addClass('hidden')
for (let i = 0; i < response.devices.length; i++) {
const device = response.devices[i];
$holder.append($('<li><a href="javascript:void(0)" class="cast-device" data-id="' + device.friendly_name + '"><i class="fa fa-brands fa-chromecast"></i>' + device.friendly_name + '</a></li>'));
}
}
});
});
$(document).on('click', '.cast-device', function () {
const $modal = $('.modal-playlist-cast-scan:visible');
const $holder = $modal.find('.cast-devices');
const $loading = $modal.find('.loading');
$holder.addClass('hidden');
$loading.removeClass('hidden');
$loading.html($loading.attr('data-casting'));
const id = $(this).attr('data-id');
$.ajax({
url: route_cast_url,
method: 'POST',
data: JSON.stringify({
device: id,
url: $('#playlist-preview-url').val()
}),
headers: {'Content-Type': 'application/json'},
success: function (response) {
$loading.addClass('hidden');
hideModal();
},
error: function () {
$loading.addClass('hidden');
$holder.removeClass('hidden');
}
});
});
main(); main();
}); });

View File

@ -19,3 +19,17 @@
transform: rotate(2deg); transform: rotate(2deg);
} }
} }
@keyframes blinkfade {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}

View File

@ -139,6 +139,30 @@
} }
} }
} }
&.highlighted:hover,
&.highlighted {
background-color: $seaBlue;
td {
font-weight: bold;
color: $gscaleF;
i.icon-legend {
color: $gscaleF;
}
span,
i.icon-value {
background-color: rgba($gscaleF, .3);
color: $gscaleF;
}
&.description {
color: $white;
}
}
}
} }
} }
} }

View File

@ -12,21 +12,71 @@
color: $gscale6; color: $gscale6;
} }
.modal-playlist-qrcode { .modal-playlist-cast-scan {
h2 { h2 {
text-align: center; text-align: left;
} }
.qrcode-pic { .alert {
padding: 10px;
font-size: 12px;
margin-bottom: 20px;
display: block;
text-align: center; text-align: center;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
img { i {
border: 4px solid $gscale5; margin-right: 5px;
}
a {
margin: 0;
}
}
.loading {
color: $gscaleF;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-name: blinkfade;
}
ul.cast-devices {
list-style: none;
margin: 0;
padding: 0;
li {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
list-style: none;
border-bottom: 1px solid $gscale2;
border-radius: $baseRadius; border-radius: $baseRadius;
a {
flex: 1;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
padding: 20px 15px;
color: $gscaleF;
align-self: stretch;
i {
margin-right: 10px;
}
}
&:hover {
background: $gscale2;
}
}
li:last-child {
border: none;
} }
} }
} }
@ -97,7 +147,7 @@
margin-bottom: 5px; margin-bottom: 5px;
} }
} }
.preview-holder { .preview-holder {
position: relative; position: relative;
@ -114,6 +164,7 @@
.hover-only { .hover-only {
display: flex; display: flex;
position: absolute; position: absolute;
&:hover { &:hover {
background: $gkscaleC; background: $gkscaleC;
} }

View File

@ -105,6 +105,7 @@
"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",
"playlist_delete_has_node_player_groups": "Playlist is linked to a playgroup", "playlist_delete_has_node_player_groups": "Playlist is linked to a playgroup",
"playlist_cast_warning": "Your <a href=\"%href%\" target=\"_blank\">external URL</a> must be served over https for this to work",
"fleet_node_player_page_title": "Players", "fleet_node_player_page_title": "Players",
"fleet_node_player_button_add": "Add a player", "fleet_node_player_button_add": "Add a player",
"fleet_node_player_panel_active": "Active players", "fleet_node_player_panel_active": "Active players",
@ -236,6 +237,7 @@
"common_pick_element": "Pick an element", "common_pick_element": "Pick an element",
"common_untitled": "<untitled>", "common_untitled": "<untitled>",
"common_loading": "Loading...", "common_loading": "Loading...",
"common_casting": "Casting...",
"common_default_node_player_group": "Default Playgroup", "common_default_node_player_group": "Default Playgroup",
"common_default_playlist": "Default Playlist", "common_default_playlist": "Default Playlist",
"common_unknown_ipaddr": "Unknown IP address", "common_unknown_ipaddr": "Unknown IP address",

View File

@ -105,6 +105,7 @@
"js_playlist_delete_confirmation": "¿Estás seguro?", "js_playlist_delete_confirmation": "¿Estás seguro?",
"playlist_delete_has_slides": "La playlist tiene diapositivas, por favor elimínelas antes y reintente", "playlist_delete_has_slides": "La playlist tiene diapositivas, por favor elimínelas antes y reintente",
"playlist_delete_has_node_player_groups": "La playlist está asignada a un playgroup", "playlist_delete_has_node_player_groups": "La playlist está asignada a un playgroup",
"playlist_cast_warning": "Tu <a href=\"%href%\" target=\"_blank\">URL externa</a> debe ser entregada en https para que esto funcione",
"fleet_node_player_page_title": "Reproductores", "fleet_node_player_page_title": "Reproductores",
"fleet_node_player_button_add": "Agregar un reproductor", "fleet_node_player_button_add": "Agregar un reproductor",
"fleet_node_player_panel_active": "Reproductores activos", "fleet_node_player_panel_active": "Reproductores activos",
@ -237,6 +238,7 @@
"common_pick_element": "Elige un elemento", "common_pick_element": "Elige un elemento",
"common_untitled": "<sin-título>", "common_untitled": "<sin-título>",
"common_loading": "Cargando...", "common_loading": "Cargando...",
"common_casting": "Casting...",
"common_default_node_player_group": "Playgroup predeterminado", "common_default_node_player_group": "Playgroup predeterminado",
"common_default_playlist": "Lista de reproducción predeterminada", "common_default_playlist": "Lista de reproducción predeterminada",
"common_unknown_ipaddr": "Dirección IP desconocida", "common_unknown_ipaddr": "Dirección IP desconocida",

View File

@ -106,6 +106,7 @@
"js_playlist_delete_confirmation": "Êtes-vous sûr ?", "js_playlist_delete_confirmation": "Êtes-vous sûr ?",
"playlist_delete_has_slides": "La playlist contient des slides, supprimez-les avant et réessayez", "playlist_delete_has_slides": "La playlist contient des slides, supprimez-les avant et réessayez",
"playlist_delete_has_node_player_groups": "La playlist est attribuée à un playgroup", "playlist_delete_has_node_player_groups": "La playlist est attribuée à un playgroup",
"playlist_cast_warning": "Votre <a href=\"%href%\" target=\"_blank\">URL externe</a> doit être servi en https pour que ça fonctionne",
"fleet_node_player_page_title": "Lecteurs", "fleet_node_player_page_title": "Lecteurs",
"fleet_node_player_button_add": "Ajouter un lecteur", "fleet_node_player_button_add": "Ajouter un lecteur",
"fleet_node_player_panel_active": "Players actifs", "fleet_node_player_panel_active": "Players actifs",
@ -238,6 +239,7 @@
"common_pick_element": "Choisissez un élément", "common_pick_element": "Choisissez un élément",
"common_untitled": "<sans-titre>", "common_untitled": "<sans-titre>",
"common_loading": "Chargement...", "common_loading": "Chargement...",
"common_casting": "Casting...",
"common_default_node_player_group": "Playgroup par défaut", "common_default_node_player_group": "Playgroup par défaut",
"common_default_playlist": "Playlist par défaut", "common_default_playlist": "Playlist par défaut",
"common_unknown_ipaddr": "Adresse IP inconnue", "common_unknown_ipaddr": "Adresse IP inconnue",

View File

@ -105,6 +105,7 @@
"js_playlist_delete_confirmation": "Sei sicuro?", "js_playlist_delete_confirmation": "Sei sicuro?",
"playlist_delete_has_slides": "Sono presenti slide nella playlist, annullale e riprova", "playlist_delete_has_slides": "Sono presenti slide nella playlist, annullale e riprova",
"playlist_delete_has_node_player_groups": "La playlist è collegata ad un playgroup", "playlist_delete_has_node_player_groups": "La playlist è collegata ad un playgroup",
"playlist_cast_warning": "Il tuo <a href=\"%href%\" target=\"_blank\">URL esterno</a> deve essere servito in https affinché funzioni",
"fleet_node_player_page_title": "Schermi", "fleet_node_player_page_title": "Schermi",
"fleet_node_player_button_add": "Aggiungi allo schermo", "fleet_node_player_button_add": "Aggiungi allo schermo",
"fleet_node_player_panel_active": "Schermi attivi", "fleet_node_player_panel_active": "Schermi attivi",
@ -237,6 +238,7 @@
"common_pick_element": "Scegli un elemento", "common_pick_element": "Scegli un elemento",
"common_untitled": "<senza-titolo>", "common_untitled": "<senza-titolo>",
"common_loading": "Caricamento...", "common_loading": "Caricamento...",
"common_casting": "Casting...",
"common_default_node_player_group": "Playgroup di default", "common_default_node_player_group": "Playgroup di default",
"common_default_playlist": "Default playlist", "common_default_playlist": "Default playlist",
"common_unknown_ipaddr": "IP sconosciuto", "common_unknown_ipaddr": "IP sconosciuto",

View File

@ -1,8 +1,10 @@
flask==2.3.3 flask==2.3.3
flask-restx==1.3.0 flask-restx==1.3.0
pychromecast==13.1.0
python-dotenv python-dotenv
cron-descriptor cron-descriptor
waitress waitress
flask-login flask-login
pysqlite3 pysqlite3
psutil psutil
zeroconf

View File

@ -1,7 +1,8 @@
from typing import Optional
from flask import Flask, send_file, render_template_string, jsonify from flask import Flask, send_file, render_template_string, jsonify, request
from src.interface.ObController import ObController from src.interface.ObController import ObController
from src.util.UtilChromecast import fetch_friendly_names, cast_url
class CoreController(ObController): class CoreController(ObController):
@ -9,6 +10,8 @@ class CoreController(ObController):
def register(self): def register(self):
self._app.add_url_rule('/manifest.json', 'manifest', self.manifest, methods=['GET']) self._app.add_url_rule('/manifest.json', 'manifest', self.manifest, methods=['GET'])
self._app.add_url_rule('/favicon.ico', 'favicon', self.favicon, methods=['GET']) self._app.add_url_rule('/favicon.ico', 'favicon', self.favicon, methods=['GET'])
self._app.add_url_rule('/cast-scan', 'cast_scan', self.cast_scan, methods=['GET'])
self._app.add_url_rule('/cast-url', 'cast_url', self.cast_url, methods=['POST'])
def manifest(self): def manifest(self):
with open("{}/manifest.jinja.json".format(self.get_template_dir()), 'r') as file: with open("{}/manifest.jinja.json".format(self.get_template_dir()), 'r') as file:
@ -19,4 +22,17 @@ class CoreController(ObController):
return self._app.response_class(rendered_content, mimetype='application/json') return self._app.response_class(rendered_content, mimetype='application/json')
def favicon(self): def favicon(self):
return send_file("{}/favicon.ico".format(self.get_web_dir()), mimetype='image/x-icon') return send_file("{}/favicon.ico".format(self.get_web_dir()), mimetype='image/x-icon')
def cast_scan(self):
return jsonify({
'devices': fetch_friendly_names(discovery_timeout=5)
})
def cast_url(self):
data = request.get_json()
success = cast_url(friendly_name=data.get('device'), url=data.get('url'))
return jsonify({
'success': success
})

View File

@ -0,0 +1,66 @@
import time
import pychromecast
import zeroconf
from pychromecast.discovery import stop_discovery
from pychromecast import CastBrowser, SimpleCastListener, get_chromecast_from_host, Chromecast
from pychromecast.controllers import BaseController
from typing import Optional
APPLICATION_ID = '81585E3E'
class CastController(BaseController):
def __init__(self):
super(CastController, self).__init__("urn:x-cast:com.jrk.obscreen")
def load_url(self, url: str):
self.send_message({
'url': url,
'type': 'load'
})
def _discover(discovery_timeout: int = 5):
zconf = zeroconf.Zeroconf()
browser = pychromecast.CastBrowser(pychromecast.SimpleCastListener(), zconf)
browser.start_discovery()
time.sleep(discovery_timeout)
stop_discovery(browser)
return browser
def fetch_friendly_names(discovery_timeout: int = 5):
return [{"friendly_name": cast_info.friendly_name} for device, cast_info in _discover(discovery_timeout).devices.items()]
def fetch_chromecast(friendly_name: str, discovery_timeout: int = 5) -> Optional[Chromecast]:
for uuid, cast_info in _discover(discovery_timeout).devices.items():
if cast_info.friendly_name == friendly_name:
try:
return get_chromecast_from_host((cast_info.host, cast_info.port, uuid, cast_info.model_name, cast_info.friendly_name))
except:
pass
return None
def cast_url(friendly_name: str, url: str, discovery_timeout: int = 5) -> bool:
chromecast = fetch_chromecast(friendly_name, discovery_timeout)
if not chromecast:
return False
chromecast.wait()
chromecast.quit_app()
time.sleep(2)
cast_controller = CastController()
chromecast.register_handler(cast_controller)
chromecast.start_app(APPLICATION_ID)
time.sleep(2)
cast_controller.load_url(url)
return True

View File

@ -1 +1 @@
2.3.0 2.3.1

View File

@ -3,6 +3,7 @@
{% set ns = namespace(last_section='') %} {% set ns = namespace(last_section='') %}
{% for variable in variables %} {% for variable in variables %}
{% set section_change = variable.section and ns.last_section != variable.section %} {% set section_change = variable.section and ns.last_section != variable.section %}
{% set highlighted = request.args.get('highlight') == variable.name %}
{% if section_change %} {% if section_change %}
{% if not loop.first %}</tbody><tbody>{% endif %} {% if not loop.first %}</tbody><tbody>{% endif %}
@ -12,7 +13,7 @@
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
<tr class="variable-item variable-edit" data-level="{{ variable.id }}" data-entity="{{ variable.to_json() }}"> <tr class="variable-item variable-edit {{ 'highlighted' if highlighted }}" data-level="{{ variable.id }}" data-entity="{{ variable.to_json() }}">
<td class="description"> <td class="description">
{{ t(variable.description) }} {{ t(variable.description) }}
</td> </td>

View File

@ -100,9 +100,12 @@
<button type="button" class="btn btn-naked copy-link" data-target-id="playlist-preview-url"> <button type="button" class="btn btn-naked copy-link" data-target-id="playlist-preview-url">
<i class="fa fa-copy"></i> <i class="fa fa-copy"></i>
</button> </button>
<a href="{{ preview_url }}" class="btn btn-neutral" target="_blank"> <a href="{{ preview_url }}" class="btn btn-info" target="_blank">
<i class="fa-solid fa-up-right-from-square"></i> <i class="fa-solid fa-up-right-from-square"></i>
</a> </a>
<button type="button" class="btn btn-neutral cast-scan">
<i class="fa fa-brands fa-chromecast"></i>
</button>
</div> </div>
</div> </div>

View File

@ -13,6 +13,8 @@
{% block add_js %} {% block add_js %}
<script type="text/javascript"> <script type="text/javascript">
var route_slide_position = '{{ url_for('slideshow_slide_position') }}'; var route_slide_position = '{{ url_for('slideshow_slide_position') }}';
var route_cast_scan = '{{ url_for('cast_scan') }}';
var route_cast_url = '{{ url_for('cast_url') }}';
var choices_translations = { var choices_translations = {
'loop': '{{ l.slideshow_slide_form_label_cron_scheduled_loop }}', 'loop': '{{ l.slideshow_slide_form_label_cron_scheduled_loop }}',
'datetime': '{{ l.slideshow_slide_form_label_cron_scheduled_datetime }}', 'datetime': '{{ l.slideshow_slide_form_label_cron_scheduled_datetime }}',
@ -127,7 +129,7 @@
<div class="modals-outer"> <div class="modals-outer">
<div class="modals-inner"> <div class="modals-inner">
{% include 'playlist/modal/add.jinja.html' %} {% include 'playlist/modal/add.jinja.html' %}
{% include 'playlist/modal/qrcode.jinja.html' %} {% include 'playlist/modal/cast-scan.jinja.html' %}
{% with is_notification=True %}{% include 'slideshow/slides/modal/edit.jinja.html' %}{% endwith %} {% with is_notification=True %}{% include 'slideshow/slides/modal/edit.jinja.html' %}{% endwith %}
{% with is_notification=False %}{% include 'slideshow/slides/modal/edit.jinja.html' %}{% endwith %} {% with is_notification=False %}{% include 'slideshow/slides/modal/edit.jinja.html' %}{% endwith %}

View File

@ -0,0 +1,25 @@
<div class="modal modal-playlist-cast-scan modal-playlist">
<h2>
Cast
</h2>
<div class="alert alert-warning">
<i class="fa fa-warning"></i>
{{ l.playlist_cast_warning|replace('%href%', url_for('settings_variable_list', highlight='external_url'))|safe }}
</div>
<div class="loading" data-loading="{{ l.common_loading }}" data-casting="{{ l.common_casting }}">
{{ l.common_loading }}
</div>
<ul class="cast-devices">
</ul>
<div class="actions actions-right">
<button type="button" class="btn btn-naked modal-close">
<i class="fa fa-close icon-left"></i>{{ l.common_close }}
</button>
</div>
</div>

View File

@ -1,13 +0,0 @@
<div class="modal modal-playlist-qrcode modal-playlist">
<h2>
{{ l.playlist_form_show_qrcode }}
</h2>
<div id="qrcode" class="qrcode-pic"></div>
<div class="actions actions-center">
<button type="button" class="btn btn-naked modal-close">
<i class="fa fa-close icon-left"></i>{{ l.common_close }}
</button>
</div>
</div>