commit
9e54506dcc
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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();
|
||||
});
|
||||
|
||||
@ -19,3 +19,17 @@
|
||||
transform: rotate(2deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blinkfade {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
})
|
||||
|
||||
66
src/util/UtilChromecast.py
Normal file
66
src/util/UtilChromecast.py
Normal 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
|
||||
@ -1 +1 @@
|
||||
2.3.0
|
||||
2.3.1
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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 %}
|
||||
|
||||
25
views/playlist/modal/cast-scan.jinja.html
Normal file
25
views/playlist/modal/cast-scan.jinja.html
Normal 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>
|
||||
@ -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>
|
||||
Loading…
Reference in New Issue
Block a user