This commit is contained in:
jr-k 2024-08-06 13:18:02 +02:00
parent 7ec157cf62
commit da8cfa9222
10 changed files with 352 additions and 207 deletions

74
data/www/js/cast-url.js Normal file
View File

@ -0,0 +1,74 @@
var applicationID = '81585E3E';
var namespace = 'urn:x-cast:com.jrk.obscreen';
var session = null;
if (!chrome.cast || !chrome.cast.isAvailable) {
setTimeout(initializeCastApi, 1000);
}
function initializeCastApi() {
var sessionRequest = new chrome.cast.SessionRequest(applicationID);
var apiConfig = new chrome.cast.ApiConfig(sessionRequest,
sessionListener,
receiverListener);
chrome.cast.initialize(apiConfig, onInitSuccess, onError);
}
function onInitSuccess() {
// console.log('onInitSuccess');
}
function onError(message) {
console.error('onError: ' + JSON.stringify(message));
}
function onSuccess(message) {
// console.log('onSuccess: ' + JSON.stringify(message));
}
// function onStopAppSuccess() {
// console.log('onStopAppSuccess');
// }
function sessionListener(e) {
console.log('New session ID: ' + e.sessionId);
session = e;
session.addUpdateListener(sessionUpdateListener);
}
function sessionUpdateListener(isAlive) {
console.log((isAlive ? 'Session Updated' : 'Session Removed') + ': ' + session.sessionId);
if (!isAlive) {
session = null;
}
}
function receiverListener(e) {
// Due to API changes just ignore this.
}
function sendMessage(message) {
if (session != null) {
session.sendMessage(namespace, message, onSuccess.bind(this, message), onError);
} else {
chrome.cast.requestSession(function (e) {
session = e;
sessionListener(e);
session.sendMessage(namespace, message, onSuccess.bind(this, message), onError);
}, onError);
}
}
// function stopApp() {
// session.stop(onStopAppSuccess, onError);
// }
jQuery(function ($) {
$(document).on('click', '.cast-url', function () {
sendMessage({
type: 'load',
url: $('#' + $(this).attr('data-target-id')).val()
});
});
});

View File

@ -0,0 +1,13 @@
(function(){/*
Copyright The Closure Library Authors.
SPDX-License-Identifier: Apache-2.0
*/
'use strict';var l=function(){var a=h,b=0;return function(){return b<a.length?{done:!1,value:a[b++]}:{done:!0}}},m=this||self,n=/^[\w+/_-]+[=]{0,2}$/,p=null,q=function(a){return(a=a.querySelector&&a.querySelector("script[nonce]"))&&(a=a.nonce||a.getAttribute("nonce"))&&n.test(a)?a:""},r=function(a,b){function e(){}e.prototype=b.prototype;a.i=b.prototype;a.prototype=new e;a.prototype.constructor=a;a.h=function(c,g,k){for(var f=Array(arguments.length-2),d=2;d<arguments.length;d++)f[d-2]=arguments[d];
return b.prototype[g].apply(c,f)}},t=function(a){return a};function u(a){if(Error.captureStackTrace)Error.captureStackTrace(this,u);else{var b=Error().stack;b&&(this.stack=b)}a&&(this.message=String(a))}r(u,Error);u.prototype.name="CustomError";var v=function(a,b){a=a.split("%s");for(var e="",c=a.length-1,g=0;g<c;g++)e+=a[g]+(g<b.length?b[g]:"%s");u.call(this,e+a[c])};r(v,u);v.prototype.name="AssertionError";var w=function(a,b){throw new v("Failure"+(a?": "+a:""),Array.prototype.slice.call(arguments,1));};var x;var A=function(a,b){this.g=b===z?a:""};A.prototype.toString=function(){return this.g+""};var z={};var B=function(){var a=window.navigator.userAgent.match(/Chrome\/([0-9]+)/);return a?parseInt(a[1],10):0},C=function(a){return!!document.currentScript&&(-1!=document.currentScript.src.indexOf("?"+a)||-1!=document.currentScript.src.indexOf("&"+a))},D=function(){return"function"==typeof window.__onGCastApiAvailable?window.__onGCastApiAvailable:null},F=function(a){a.length?E(a.shift(),function(){F(a)}):G()},H=function(a){return"chrome-extension://"+a+"/cast_sender.js"},E=function(a,b,e){var c=document.createElement("script");
c.onerror=b;e&&(c.onload=e);if(void 0===x)if(b=null,(e=m.trustedTypes)&&e.createPolicy){try{b=e.createPolicy("goog#html",{createHTML:t,createScript:t,createScriptURL:t})}catch(y){m.console&&m.console.error(y.message)}x=b}else x=b;a=(b=x)?b.createScriptURL(a):a;a=new A(a,z);a:{try{var g=c&&c.ownerDocument,k=g&&(g.defaultView||g.parentWindow);k=k||m;if(k.Element&&k.Location){var f=k;break a}}catch(y){}f=null}if(f&&"undefined"!=typeof f.HTMLScriptElement&&(!c||!(c instanceof f.HTMLScriptElement)&&(c instanceof
f.Location||c instanceof f.Element))){f=typeof c;if("object"==f&&null!=c||"function"==f)try{var d=c.constructor.displayName||c.constructor.name||Object.prototype.toString.call(c)}catch(y){d="<object could not be stringified>"}else d=void 0===c?"undefined":null===c?"null":typeof c;w("Argument is not a %s (or a non-Element, non-Location mock); got: %s","HTMLScriptElement",d)}a instanceof A&&a.constructor===A?d=a.g:(d=typeof a,w("expected object of type TrustedResourceUrl, got '"+a+"' of type "+("object"!=
d?d:a?Array.isArray(a)?"array":d:"null")),d="type_error:TrustedResourceUrl");c.src=d;(d=c.ownerDocument&&c.ownerDocument.defaultView)&&d!=m?d=q(d.document):(null===p&&(p=q(m.document)),d=p);d&&c.setAttribute("nonce",d);(document.head||document.documentElement).appendChild(c)},I=function(){var a=B(),b=[];if(1<a){var e=a-1;b.push("https://www.gstatic.com/eureka/clank/"+a+"/cast_sender.js");b.push("https://www.gstatic.com/eureka/clank/"+e+"/cast_sender.js")}return b},G=function(){var a=D();a&&a(!1,"No cast extension found")},
K=function(){if(J){var a=2,b=D(),e=function(){a--;0==a&&b&&b(!0)};window.__onGCastApiAvailable=e;E("https://www.gstatic.com/cast/sdk/libs/sender/1.0/cast_framework.js",G,e)}},J=C("loadCastFramework")||C("loadCastApplicationFramework"),L=["pkedcjkdefgpdelpbcmbmeomcjbeemfm","enhhojjnijigcajfphajepfemndkmdlo"];if(0<=window.navigator.userAgent.indexOf("Android")&&0<=window.navigator.userAgent.indexOf("Chrome/")&&window.navigator.presentation){if(60<=B()){K();var M=I();M.push("https://www.gstatic.com/eureka/clank/cast_sender.js");F(M)}}else if(!window.chrome||!window.navigator.presentation||0<=window.navigator.userAgent.indexOf("Edge"))G();else if(89<=B()){K();var N=I(),O=N.push,P=O.apply,h=L.map(H),Q;if(h instanceof Array)Q=h;else{var R,S="undefined"!=typeof Symbol&&Symbol.iterator&&h[Symbol.iterator];R=S?S.call(h):
{next:l()};for(var T,U=[];!(T=R.next()).done;)U.push(T.value);Q=U}P.call(O,N,Q);N.push("https://www.gstatic.com/eureka/clank/cast_sender.js");F(N)}else K(),F(L.map(H));}).call(this);

View File

@ -40,62 +40,62 @@ jQuery(document).ready(function ($) {
$icon.removeClass('fa-pause').addClass('fa-play'); $icon.removeClass('fa-pause').addClass('fa-play');
} }
}); });
//
// $(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-scan', function () { // $(document).on('click', '.cast-device', function () {
showModal('modal-playlist-cast-scan'); // const $modal = $('.modal-playlist-cast-scan:visible');
const $modal = $('.modal-playlist-cast-scan:visible'); // const $holder = $modal.find('.cast-devices');
const $holder = $modal.find('.cast-devices'); // const $loading = $modal.find('.loading');
const $loading = $modal.find('.loading'); //
// $holder.addClass('hidden');
$loading.removeClass('hidden'); // $loading.removeClass('hidden');
$holder.removeClass('hidden'); // $loading.html($loading.attr('data-casting'));
$holder.html(''); //
$loading.html($loading.attr('data-loading')); // const id = $(this).attr('data-id');
//
$.ajax({ // $.ajax({
method: 'GET', // url: route_cast_url,
url: route_cast_scan, // method: 'POST',
headers: {'Content-Type': 'application/json'}, // data: JSON.stringify({
success: function (response) { // device: id,
$loading.addClass('hidden') // url: $('#playlist-preview-url').val()
// }),
for (let i = 0; i < response.devices.length; i++) { // headers: {'Content-Type': 'application/json'},
const device = response.devices[i]; // success: function (response) {
$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>')); // $loading.addClass('hidden');
} // hideModal();
} // },
}); // error: function () {
}); // $loading.addClass('hidden');
// $holder.removeClass('hidden');
$(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

@ -11,75 +11,75 @@
align-self: stretch; align-self: stretch;
color: $gscale6; color: $gscale6;
} }
//
.modal-playlist-cast-scan { //.modal-playlist-cast-scan {
h2 { // h2 {
text-align: left; // text-align: left;
} // }
//
.alert { // .alert {
padding: 10px; // padding: 10px;
font-size: 12px; // font-size: 12px;
margin-bottom: 20px; // margin-bottom: 20px;
display: block; // display: block;
text-align: center; // text-align: center;
//
i { // i {
margin-right: 5px; // margin-right: 5px;
} // }
//
a { // a {
margin: 0; // margin: 0;
} // }
} // }
//
.loading { // .loading {
color: $gscaleF; // color: $gscaleF;
animation-duration: 2s; // animation-duration: 2s;
animation-iteration-count: infinite; // animation-iteration-count: infinite;
animation-name: blinkfade; // animation-name: blinkfade;
} // }
//
ul.cast-devices { // ul.cast-devices {
list-style: none; // list-style: none;
margin: 0; // margin: 0;
padding: 0; // padding: 0;
//
li { // li {
display: flex; // display: flex;
flex-direction: row; // flex-direction: row;
justify-content: flex-start; // justify-content: flex-start;
align-items: center; // align-items: center;
list-style: none; // list-style: none;
border-bottom: 1px solid $gscale2; // border-bottom: 1px solid $gscale2;
border-radius: $baseRadius; // border-radius: $baseRadius;
//
a { // a {
flex: 1; // flex: 1;
display: flex; // display: flex;
flex-direction: row; // flex-direction: row;
justify-content: flex-start; // justify-content: flex-start;
align-items: center; // align-items: center;
padding: 20px 15px; // padding: 20px 15px;
color: $gscaleF; // color: $gscaleF;
align-self: stretch; // align-self: stretch;
//
i { // i {
//
margin-right: 10px; // margin-right: 10px;
} // }
} // }
//
&:hover { // &:hover {
background: $gscale2; // background: $gscale2;
} // }
} // }
//
li:last-child { // li:last-child {
border: none; // border: none;
} // }
} // }
} //}
.modal-slide { .modal-slide {
h2 { h2 {

42
docker/nginx/nginx.conf Normal file
View File

@ -0,0 +1,42 @@
user nginx;
worker_processes 4;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /var/log/nginx/access.log;
sendfile on;
keepalive_timeout 65;
client_max_body_size 200G;
autoindex on;
server {
root /var/www/html/public;
listen 80 default_server;
listen 443 ssl default_server;
ssl_certificate /ssl/ssl-cert-snakeoil.pem;
ssl_certificate_key /ssl/ssl-cert-snakeoil.key;
location / {
proxy_connect_timeout 60;
proxy_read_timeout 60;
proxy_send_timeout 60;
proxy_intercept_errors on;
proxy_http_version 1.1;
proxy_pass http://localhost:5000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}

View File

@ -1,10 +1,8 @@
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

@ -10,8 +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-scan', 'cast_scan', self.cast_scan, methods=['GET'])
self._app.add_url_rule('/cast-url', 'cast_url', self.cast_url, methods=['POST']) # 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:
@ -24,14 +24,14 @@ class CoreController(ObController):
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): # def cast_scan(self):
return jsonify({ # return jsonify({
'devices': fetch_friendly_names(discovery_timeout=5) # 'devices': fetch_friendly_names(discovery_timeout=5)
}) # })
#
def cast_url(self): # def cast_url(self):
data = request.get_json() # data = request.get_json()
success = cast_url(friendly_name=data.get('device'), url=data.get('url'), discovery_timeout=5) # success = cast_url(friendly_name=data.get('device'), url=data.get('url'), discovery_timeout=5)
return jsonify({ return jsonify({
'success': success 'success': success

View File

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

View File

@ -103,9 +103,12 @@
<a href="{{ preview_url }}" class="btn btn-info" 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"> <button type="button" class="btn btn-neutral cast-url chrome-only hidden" data-target-id="playlist-preview-url">
<i class="fa fa-brands fa-chromecast"></i> <i class="fa fa-brands fa-chromecast"></i>
</button> </button>
{# <button type="button" class="btn btn-neutral cast-scan">#}
{# <i class="fa fa-brands fa-chromecast"></i>#}
{# </button>#}
</div> </div>
</div> </div>

View File

@ -46,6 +46,21 @@
} }
var contents = {{ json_dumps(contents) | safe }} var contents = {{ json_dumps(contents) | safe }}
</script> </script>
<script>
jQuery(function($) {
if (typeof chrome !== 'undefined') {
$('.chrome-only').removeClass('hidden');
var script_sender = document.createElement('script');
script_sender.src = "{{ STATIC_PREFIX }}js/lib/cast-sender.js";
document.body.appendChild(script_sender);
var script_caster = document.createElement('script');
script_caster.src = "{{ STATIC_PREFIX }}js/cast-url.js";
document.body.appendChild(script_caster);
}
});
</script>
<script src="{{ STATIC_PREFIX }}js/lib/jquery-explr-1.4.js"></script> <script src="{{ STATIC_PREFIX }}js/lib/jquery-explr-1.4.js"></script>
<script src="{{ STATIC_PREFIX }}js/lib/jquery-ui.min.js"></script> <script src="{{ STATIC_PREFIX }}js/lib/jquery-ui.min.js"></script>
<script src="{{ STATIC_PREFIX }}js/lib/flatpickr.min.js"></script> <script src="{{ STATIC_PREFIX }}js/lib/flatpickr.min.js"></script>
@ -129,7 +144,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/cast-scan.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 %}