Browse Source

Use own style reloader

pull/5180/head
Ruud 11 years ago
parent
commit
6ccbad031f
  1. 52
      couchpotato/core/_base/clientscript.py
  2. 8
      couchpotato/static/style/main.scss
  3. 8
      couchpotato/templates/index.html
  4. 16
      libs/livereload/__init__.py
  5. 43
      libs/livereload/_compat.py
  6. 209
      libs/livereload/handlers.py
  7. 1055
      libs/livereload/livereload.js
  8. 224
      libs/livereload/server.py
  9. 132
      libs/livereload/watcher.py

52
couchpotato/core/_base/clientscript.py

@ -53,7 +53,7 @@ class ClientScript(Plugin):
],
}
watcher = None
watches = {}
original_paths = {'style': {}, 'script': {}}
paths = {'style': {}, 'script': {}}
@ -82,17 +82,7 @@ class ClientScript(Plugin):
def livereload(self):
if Env.get('dev'):
from livereload import Server
from livereload.watcher import Watcher
self.livereload_server = Server()
self.livereload_server.watch('%s/minified/*.css' % Env.get('cache_dir'))
self.livereload_server.watch('%s/*.css' % os.path.join(Env.get('app_dir'), 'couchpotato', 'static', 'style'))
self.watcher = Watcher()
fireEvent('schedule.interval', 'livereload.watcher', self.watcher.examine, seconds = .5)
self.livereload_server.serve(port = 35729)
fireEvent('schedule.interval', 'livereload.watcher', self.watcher, seconds = .5)
def addCore(self):
@ -106,6 +96,39 @@ class ClientScript(Plugin):
else:
self.registerStyle(core_url, file_path, position = 'front')
def watcher(self):
changed = []
for file_path in self.watches:
info = self.watches[file_path]
old_time = info['file_time']
file_time = os.path.getmtime(file_path)
if file_time > old_time:
changed.append(info['api_path'])
if info['compiled_path']:
compiler = Scss(live_errors = True, search_paths = [os.path.dirname(file_path)])
f = open(file_path, 'r').read()
f = compiler.compile(f)
self.createFile(info['compiled_path'], f.strip())
# Add file to watchlist again, with current filetime
self.watches[file_path]['file_time'] = file_time
# Notify fronted with changes
if changed:
fireEvent('notify.frontend', type = 'watcher.changed', data = changed)
def watchFile(self, file_path, api_path, compiled_path = False):
self.watches[file_path] = {
'file_time': os.path.getmtime(file_path),
'api_path': api_path,
'compiled_path': compiled_path,
}
def compile(self):
# Create cache dir
@ -146,7 +169,6 @@ class ClientScript(Plugin):
# Reload watcher
if Env.get('dev'):
self.watcher.watch(file_path, self.compile)
compiled_file_name = position + '_%s.css' % url_path.replace('/', '_').split('.scss')[0]
compiled_file_path = os.path.join(minified_dir, compiled_file_name)
@ -155,6 +177,8 @@ class ClientScript(Plugin):
# Remove scss path
x = (file_path, 'minified/%s?%s' % (compiled_file_name, tryInt(time.time())))
self.watchFile(file_path, x[1], compiled_path = compiled_file_path)
if not Env.get('dev'):
if file_type == 'script':
@ -199,6 +223,8 @@ class ClientScript(Plugin):
def register(self, api_path, file_path, type, location):
self.watchFile(file_path, api_path)
api_path = '%s?%s' % (api_path, tryInt(os.path.getmtime(file_path)))
if not self.original_paths[type].get(location):

8
couchpotato/static/style/main.scss

@ -5,7 +5,7 @@ body, html {
font-size: 14px;
line-height: 1.5;
font-family: OpenSans, "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif;
height: 100%;
height: 100%;
margin: 0;
padding: 0;
background: #111;
@ -75,6 +75,11 @@ a {
border-radius: 3px 0 0 3px;
overflow: hidden;
h1, h2, h3 {
padding: 0;
margin: 0;
}
.pages {
height: 100%;
widows: 100%;
@ -91,6 +96,7 @@ a {
.page {
display: none;
padding: 20px;
&.active {
display: block;

8
couchpotato/templates/index.html

@ -8,18 +8,22 @@
{% for url in fireEvent('clientscript.get_styles', location = 'front', single = True) %}
<link rel="stylesheet" href="{{ Env.get('web_base') }}{{ url }}" type="text/css">{% end %}
<link data-url="{{ url }}" rel="stylesheet" href="{{ Env.get('web_base') }}{{ url }}" type="text/css">{% end %}
{% for url in fireEvent('clientscript.get_scripts', location = 'front', single = True) %}
<script type="text/javascript" src="{{ Env.get('web_base') }}{{ url }}"></script>{% end %}
{% for url in fireEvent('clientscript.get_scripts', location = 'head', single = True) %}
<script type="text/javascript" src="{{ Env.get('web_base') }}{{ url }}"></script>{% end %}
{% for url in fireEvent('clientscript.get_styles', location = 'head', single = True) %}
<link rel="stylesheet" href="{{ Env.get('web_base') }}{{ url }}" type="text/css">{% end %}
<link data-url="{{ url }}" rel="stylesheet" href="{{ Env.get('web_base') }}{{ url }}" type="text/css">{% end %}
<link href="{{ Env.get('static_path') }}images/favicon.ico" rel="icon" type="image/x-icon" />
<link rel="apple-touch-icon" href="{{ Env.get('static_path') }}images/homescreen.png" />
{% if Env.get('dev') %}
<script type="text/javascript" src="{{ Env.get('static_path') }}scripts/reloader.js"></script>
{% end %}
<script type="text/javascript" src="https://www.youtube.com/player_api" defer="defer"></script>
<script type="text/javascript">

16
libs/livereload/__init__.py

@ -1,16 +0,0 @@
"""
livereload
~~~~~~~~~~
A python version of livereload.
:copyright: (c) 2013 by Hsiaoming Yang
"""
__version__ = '2.2.0'
__author__ = 'Hsiaoming Yang <me@lepture.com>'
__homepage__ = 'https://github.com/lepture/python-livereload'
from .server import Server, shell
__all__ = ('Server', 'shell')

43
libs/livereload/_compat.py

@ -1,43 +0,0 @@
# coding: utf-8
"""
livereload._compat
~~~~~~~~~~~~~~~~~~
Compatible module for python2 and python3.
:copyright: (c) 2013 by Hsiaoming Yang
"""
import sys
PY3 = sys.version_info[0] == 3
if PY3:
unicode_type = str
bytes_type = bytes
text_types = (str,)
else:
unicode_type = unicode
bytes_type = str
text_types = (str, unicode)
def to_unicode(value, encoding='utf-8'):
"""Convert different types of objects to unicode."""
if isinstance(value, unicode_type):
return value
if isinstance(value, bytes_type):
return unicode_type(value, encoding=encoding)
if isinstance(value, int):
return unicode_type(str(value))
return value
def to_bytes(value, encoding='utf-8'):
"""Convert different types of objects to bytes."""
if isinstance(value, bytes_type):
return value
return value.encode(encoding)

209
libs/livereload/handlers.py

@ -1,209 +0,0 @@
# -*- coding: utf-8 -*-
"""
livereload.handlers
~~~~~~~~~~~~~~~~~~~
HTTP and WebSocket handlers for livereload.
:copyright: (c) 2013 by Hsiaoming Yang
"""
import os
import time
import hashlib
import logging
import mimetypes
from tornado import ioloop
from tornado import escape
from tornado.websocket import WebSocketHandler
from tornado.web import RequestHandler
from tornado.util import ObjectDict
from ._compat import to_bytes
class LiveReloadHandler(WebSocketHandler):
waiters = set()
watcher = None
_last_reload_time = None
def allow_draft76(self):
return True
def on_close(self):
if self in LiveReloadHandler.waiters:
LiveReloadHandler.waiters.remove(self)
def send_message(self, message):
if isinstance(message, dict):
message = escape.json_encode(message)
try:
self.write_message(message)
except:
logging.error('Error sending message', exc_info=True)
def poll_tasks(self):
filepath = self.watcher.examine()
if not filepath:
return
logging.info('File %s changed', filepath)
self.watch_tasks()
def watch_tasks(self):
if time.time() - self._last_reload_time < 3:
# if you changed lot of files in one time
# it will refresh too many times
logging.info('ignore this reload action')
return
logging.info('Reload %s waiters', len(self.waiters))
msg = {
'command': 'reload',
'path': self.watcher.filepath or '*',
'liveCSS': True
}
self._last_reload_time = time.time()
for waiter in LiveReloadHandler.waiters:
try:
waiter.write_message(msg)
except:
logging.error('Error sending message', exc_info=True)
LiveReloadHandler.waiters.remove(waiter)
def on_message(self, message):
"""Handshake with livereload.js
1. client send 'hello'
2. server reply 'hello'
3. client send 'info'
http://feedback.livereload.com/knowledgebase/articles/86174-livereload-protocol
"""
message = ObjectDict(escape.json_decode(message))
if message.command == 'hello':
handshake = {}
handshake['command'] = 'hello'
handshake['protocols'] = [
'http://livereload.com/protocols/official-7',
'http://livereload.com/protocols/official-8',
'http://livereload.com/protocols/official-9',
'http://livereload.com/protocols/2.x-origin-version-negotiation',
'http://livereload.com/protocols/2.x-remote-control'
]
handshake['serverName'] = 'livereload-tornado'
self.send_message(handshake)
if message.command == 'info' and 'url' in message:
logging.info('Browser Connected: %s' % message.url)
LiveReloadHandler.waiters.add(self)
if not LiveReloadHandler._last_reload_time:
if not self.watcher._tasks:
logging.info('Watch current working directory')
self.watcher.watch(os.getcwd())
LiveReloadHandler._last_reload_time = time.time()
logging.info('Start watching changes')
if not self.watcher.start(self.poll_tasks):
ioloop.PeriodicCallback(self.poll_tasks, 800).start()
class LiveReloadJSHandler(RequestHandler):
def initialize(self, port):
self._port = port
def get(self):
js = os.path.join(
os.path.abspath(os.path.dirname(__file__)), 'livereload.js',
)
self.set_header('Content-Type', 'application/javascript')
with open(js, 'r') as f:
content = f.read()
content = content.replace('{{port}}', str(self._port))
self.write(content)
class ForceReloadHandler(RequestHandler):
def get(self):
msg = {
'command': 'reload',
'path': self.get_argument('path', default=None) or '*',
'liveCSS': True,
'liveImg': True
}
for waiter in LiveReloadHandler.waiters:
try:
waiter.write_message(msg)
except:
logging.error('Error sending message', exc_info=True)
LiveReloadHandler.waiters.remove(waiter)
self.write('ok')
class StaticHandler(RequestHandler):
def initialize(self, root, fallback=None):
self._root = os.path.abspath(root)
self._fallback = fallback
def filepath(self, url):
url = url.lstrip('/')
url = os.path.join(self._root, url)
if url.endswith('/'):
url += 'index.html'
elif not os.path.exists(url) and not url.endswith('.html'):
url += '.html'
if not os.path.isfile(url):
return None
return url
def get(self, path='/'):
filepath = self.filepath(path)
if not filepath and path.endswith('/'):
rootdir = os.path.join(self._root, path.lstrip('/'))
return self.create_index(rootdir)
if not filepath:
if self._fallback:
self._fallback(self.request)
self._finished = True
return
return self.send_error(404)
mime_type, encoding = mimetypes.guess_type(filepath)
if not mime_type:
mime_type = 'text/html'
self.mime_type = mime_type
self.set_header('Content-Type', mime_type)
with open(filepath, 'r') as f:
data = f.read()
hasher = hashlib.sha1()
hasher.update(to_bytes(data))
self.set_header('Etag', '"%s"' % hasher.hexdigest())
ua = self.request.headers.get('User-Agent', 'bot').lower()
if mime_type == 'text/html' and 'msie' not in ua:
data = data.replace(
'</head>',
'<script src="/livereload.js"></script></head>'
)
self.write(data)
def create_index(self, root):
files = os.listdir(root)
self.write('<ul>')
for f in files:
path = os.path.join(root, f)
self.write('<li>')
if os.path.isdir(path):
self.write('<a href="%s/">%s</a>' % (f, f))
else:
self.write('<a href="%s">%s</a>' % (f, f))
self.write('</li>')
self.write('</ul>')

1055
libs/livereload/livereload.js

File diff suppressed because it is too large

224
libs/livereload/server.py

@ -1,224 +0,0 @@
# -*- coding: utf-8 -*-
"""
livereload.server
~~~~~~~~~~~~~~~~~
WSGI app server for livereload.
:copyright: (c) 2013 by Hsiaoming Yang
"""
import os
import logging
from subprocess import Popen, PIPE
import time
import threading
import webbrowser
from tornado import escape
from tornado.wsgi import WSGIContainer
from tornado.ioloop import IOLoop
from tornado.web import Application, FallbackHandler
from .handlers import LiveReloadHandler, LiveReloadJSHandler
from .handlers import ForceReloadHandler, StaticHandler
from .watcher import Watcher
from ._compat import text_types
from tornado.log import enable_pretty_logging
enable_pretty_logging()
def shell(command, output=None, mode='w'):
"""Command shell command.
You can add a shell command::
server.watch(
'style.less', shell('lessc style.less', output='style.css')
)
:param command: a shell command, string or list
:param output: output stdout to the given file
:param mode: only works with output, mode ``w`` means write,
mode ``a`` means append
"""
if not output:
output = os.devnull
else:
folder = os.path.dirname(output)
if folder and not os.path.isdir(folder):
os.makedirs(folder)
if isinstance(command, (list, tuple)):
cmd = command
else:
cmd = command.split()
def run_shell():
try:
p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
except OSError as e:
logging.error(e)
if e.errno == os.errno.ENOENT: # file (command) not found
logging.error("maybe you haven't installed %s", cmd[0])
return e
stdout, stderr = p.communicate()
if stderr:
logging.error(stderr)
return stderr
#: stdout is bytes, decode for python3
code = stdout.decode()
with open(output, mode) as f:
f.write(code)
return run_shell
class WSGIWrapper(WSGIContainer):
"""Insert livereload scripts into response body."""
def __call__(self, request):
data = {}
response = []
def start_response(status, response_headers, exc_info=None):
data["status"] = status
data["headers"] = response_headers
return response.append
app_response = self.wsgi_application(
WSGIContainer.environ(request), start_response)
try:
response.extend(app_response)
body = b"".join(response)
finally:
if hasattr(app_response, "close"):
app_response.close()
if not data:
raise Exception("WSGI app did not call start_response")
status_code = int(data["status"].split()[0])
headers = data["headers"]
header_set = set(k.lower() for (k, v) in headers)
body = escape.utf8(body)
body = body.replace(
b'</head>',
b'<script src="/livereload.js"></script></head>'
)
if status_code != 304:
if "content-length" not in header_set:
headers.append(("Content-Length", str(len(body))))
if "content-type" not in header_set:
headers.append(("Content-Type", "text/html; charset=UTF-8"))
if "server" not in header_set:
headers.append(("Server", "livereload-tornado"))
parts = [escape.utf8("HTTP/1.1 " + data["status"] + "\r\n")]
for key, value in headers:
if key.lower() == 'content-length':
value = str(len(body))
parts.append(
escape.utf8(key) + b": " + escape.utf8(value) + b"\r\n"
)
parts.append(b"\r\n")
parts.append(body)
request.write(b"".join(parts))
request.finish()
self._log(status_code, request)
class Server(object):
"""Livereload server interface.
Initialize a server and watch file changes::
server = Server(wsgi_app)
server.serve()
:param app: a wsgi application instance
:param watcher: A Watcher instance, you don't have to initialize
it by yourself. Under Linux, you will want to install
pyinotify and use INotifyWatcher() to avoid wasted
CPU usage.
"""
def __init__(self, app=None, watcher=None):
self.app = app
self.port = 5500
self.root = None
if not watcher:
watcher = Watcher()
self.watcher = watcher
def watch(self, filepath, func=None):
"""Add the given filepath for watcher list.
Once you have intialized a server, watch file changes before
serve the server::
server.watch('static/*.stylus', 'make static')
def alert():
print('foo')
server.watch('foo.txt', alert)
server.serve()
:param filepath: files to be watched, it can be a filepath,
a directory, or a glob pattern
:param func: the function to be called, it can be a string of
shell command, or any callable object without
parameters
"""
if isinstance(func, text_types):
func = shell(func)
self.watcher.watch(filepath, func)
def application(self, debug=True):
LiveReloadHandler.watcher = self.watcher
handlers = [
(r'/livereload', LiveReloadHandler),
(r'/forcereload', ForceReloadHandler),
(r'/livereload.js', LiveReloadJSHandler, dict(port=self.port)),
]
if self.app:
self.app = WSGIWrapper(self.app)
handlers.append(
(r'.*', FallbackHandler, dict(fallback=self.app))
)
else:
handlers.append(
(r'(.*)', StaticHandler, dict(root=self.root or '.')),
)
return Application(handlers=handlers, debug=debug)
def serve(self, port=None, host=None, root=None, debug=True, open_url=False):
"""Start serve the server with the given port.
:param port: serve on this port, default is 5500
:param host: serve on this hostname, default is 0.0.0.0
:param root: serve static on this root directory
:param open_url: open system browser
"""
if root:
self.root = root
if port:
self.port = port
if host is None:
host = ''
self.application(debug=debug).listen(self.port, address=host)
logging.getLogger().setLevel(logging.INFO)
host = host or '127.0.0.1'
print('Serving on %s:%s' % (host, self.port))
# Async open web browser after 5 sec timeout
if open_url:
def opener():
time.sleep(5)
webbrowser.open('http://%s:%s' % (host, self.port))
threading.Thread(target=opener).start()
try:
IOLoop.instance().start()
except KeyboardInterrupt:
print('Shutting down...')

132
libs/livereload/watcher.py

@ -1,132 +0,0 @@
# -*- coding: utf-8 -*-
"""
livereload.watcher
~~~~~~~~~~~~~~~~~~
A file watch management for LiveReload Server.
:copyright: (c) 2013 by Hsiaoming Yang
"""
import os
import glob
import time
class Watcher(object):
"""A file watcher registery."""
def __init__(self):
self._tasks = {}
self._mtimes = {}
# filepath that is changed
self.filepath = None
self._start = time.time()
def ignore(self, filename):
"""Ignore a given filename or not."""
_, ext = os.path.splitext(filename)
return ext in ['.pyc', '.pyo', '.o', '.swp']
def watch(self, path, func=None):
"""Add a task to watcher."""
self._tasks[path] = func
def start(self, callback):
"""Start the watcher running, calling callback when changes are observed. If this returns False,
regular polling will be used."""
return False
def examine(self):
"""Check if there are changes, if true, run the given task."""
# clean filepath
self.filepath = None
for path in self._tasks:
if self.is_changed(path):
func = self._tasks[path]
# run function
func and func()
return self.filepath
def is_changed(self, path):
if os.path.isfile(path):
return self.is_file_changed(path)
elif os.path.isdir(path):
return self.is_folder_changed(path)
return self.is_glob_changed(path)
def is_file_changed(self, path):
if not os.path.isfile(path):
return False
if self.ignore(path):
return False
mtime = os.path.getmtime(path)
if path not in self._mtimes:
self._mtimes[path] = mtime
self.filepath = path
return mtime > self._start
if self._mtimes[path] != mtime:
self._mtimes[path] = mtime
self.filepath = path
return True
self._mtimes[path] = mtime
return False
def is_folder_changed(self, path):
for root, dirs, files in os.walk(path, followlinks=True):
if '.git' in dirs:
dirs.remove('.git')
if '.hg' in dirs:
dirs.remove('.hg')
if '.svn' in dirs:
dirs.remove('.svn')
if '.cvs' in dirs:
dirs.remove('.cvs')
for f in files:
if self.is_file_changed(os.path.join(root, f)):
return True
return False
def is_glob_changed(self, path):
for f in glob.glob(path):
if self.is_file_changed(f):
return True
return False
class INotifyWatcher(Watcher):
def __init__(self):
Watcher.__init__(self)
import pyinotify
self.wm = pyinotify.WatchManager()
self.notifier = None
self.callback = None
def watch(self, path, func=None):
import pyinotify
flag = pyinotify.IN_CREATE | pyinotify.IN_DELETE | pyinotify.IN_MODIFY
self.wm.add_watch(path, flag, rec=True, do_glob=True, auto_add=True)
Watcher.watch(self, path, func)
def inotify_event(self, event):
self.callback()
def start(self, callback):
if not self.notifier:
self.callback = callback
import pyinotify
from tornado import ioloop
self.notifier = pyinotify.TornadoAsyncNotifier(
self.wm, ioloop.IOLoop.instance(),
default_proc_fun=self.inotify_event
)
callback()
return True
Loading…
Cancel
Save