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();
});

View File

@ -19,3 +19,17 @@
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;
}
.modal-playlist-qrcode {
.modal-playlist-cast-scan {
h2 {
text-align: center;
text-align: left;
}
.qrcode-pic {
.alert {
padding: 10px;
font-size: 12px;
margin-bottom: 20px;
display: block;
text-align: center;
i {
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: center;
justify-content: flex-start;
align-items: center;
img {
border: 4px solid $gscale5;
list-style: none;
border-bottom: 1px solid $gscale2;
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;
}
}
}
@ -114,6 +164,7 @@
.hover-only {
display: flex;
position: absolute;
&:hover {
background: $gkscaleC;
}

View File

@ -105,6 +105,7 @@
"js_playlist_delete_confirmation": "Are you sure?",
"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_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_button_add": "Add a player",
"fleet_node_player_panel_active": "Active players",
@ -236,6 +237,7 @@
"common_pick_element": "Pick an element",
"common_untitled": "<untitled>",
"common_loading": "Loading...",
"common_casting": "Casting...",
"common_default_node_player_group": "Default Playgroup",
"common_default_playlist": "Default Playlist",
"common_unknown_ipaddr": "Unknown IP address",

View File

@ -105,6 +105,7 @@
"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_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_button_add": "Agregar un reproductor",
"fleet_node_player_panel_active": "Reproductores activos",
@ -237,6 +238,7 @@
"common_pick_element": "Elige un elemento",
"common_untitled": "<sin-título>",
"common_loading": "Cargando...",
"common_casting": "Casting...",
"common_default_node_player_group": "Playgroup predeterminado",
"common_default_playlist": "Lista de reproducción predeterminada",
"common_unknown_ipaddr": "Dirección IP desconocida",

View File

@ -106,6 +106,7 @@
"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_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_button_add": "Ajouter un lecteur",
"fleet_node_player_panel_active": "Players actifs",
@ -238,6 +239,7 @@
"common_pick_element": "Choisissez un élément",
"common_untitled": "<sans-titre>",
"common_loading": "Chargement...",
"common_casting": "Casting...",
"common_default_node_player_group": "Playgroup par défaut",
"common_default_playlist": "Playlist par défaut",
"common_unknown_ipaddr": "Adresse IP inconnue",

View File

@ -105,6 +105,7 @@
"js_playlist_delete_confirmation": "Sei sicuro?",
"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_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_button_add": "Aggiungi allo schermo",
"fleet_node_player_panel_active": "Schermi attivi",
@ -237,6 +238,7 @@
"common_pick_element": "Scegli un elemento",
"common_untitled": "<senza-titolo>",
"common_loading": "Caricamento...",
"common_casting": "Casting...",
"common_default_node_player_group": "Playgroup di default",
"common_default_playlist": "Default playlist",
"common_unknown_ipaddr": "IP sconosciuto",

View File

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

View File

@ -1,7 +1,8 @@
from flask import Flask, send_file, render_template_string, jsonify
from typing import Optional
from flask import Flask, send_file, render_template_string, jsonify, request
from src.interface.ObController import ObController
from src.util.UtilChromecast import fetch_friendly_names, cast_url
class CoreController(ObController):
@ -9,6 +10,8 @@ class CoreController(ObController):
def register(self):
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('/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):
with open("{}/manifest.jinja.json".format(self.get_template_dir()), 'r') as file:
@ -20,3 +23,16 @@ class CoreController(ObController):
def favicon(self):
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='') %}
{% for variable in variables %}
{% set section_change = variable.section and ns.last_section != variable.section %}
{% set highlighted = request.args.get('highlight') == variable.name %}
{% if section_change %}
{% if not loop.first %}</tbody><tbody>{% endif %}
@ -12,7 +13,7 @@
</td>
</tr>
{% 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">
{{ t(variable.description) }}
</td>

View File

@ -100,9 +100,12 @@
<button type="button" class="btn btn-naked copy-link" data-target-id="playlist-preview-url">
<i class="fa fa-copy"></i>
</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>
</a>
<button type="button" class="btn btn-neutral cast-scan">
<i class="fa fa-brands fa-chromecast"></i>
</button>
</div>
</div>

View File

@ -13,6 +13,8 @@
{% block add_js %}
<script type="text/javascript">
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 = {
'loop': '{{ l.slideshow_slide_form_label_cron_scheduled_loop }}',
'datetime': '{{ l.slideshow_slide_form_label_cron_scheduled_datetime }}',
@ -127,7 +129,7 @@
<div class="modals-outer">
<div class="modals-inner">
{% 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=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>