code is now fully python3 compatible (python2 is no longer supported)pull/20/merge
@@ -53,3 +53,4 @@ docs/_build/ | |||||
# PyBuilder | # PyBuilder | ||||
target/ | target/ | ||||
.idea/ |
@@ -1,7 +1,7 @@ | |||||
# Copyright 2015 Adafruit Industries. | # Copyright 2015 Adafruit Industries. | ||||
# Author: Tony DiCola | # Author: Tony DiCola | ||||
# License: GNU GPLv2, see LICENSE.txt | # License: GNU GPLv2, see LICENSE.txt | ||||
class DirectoryReader(object): | |||||
class DirectoryReader: | |||||
def __init__(self, config): | def __init__(self, config): | ||||
"""Create an instance of a file reader that just reads a single | """Create an instance of a file reader that just reads a single | ||||
@@ -6,7 +6,7 @@ import subprocess | |||||
import time | import time | ||||
class HelloVideoPlayer(object): | |||||
class HelloVideoPlayer: | |||||
def __init__(self, config): | def __init__(self, config): | ||||
"""Create an instance of a video player that runs hello_video.bin in the | """Create an instance of a video player that runs hello_video.bin in the | ||||
@@ -17,7 +17,7 @@ class HelloVideoPlayer(object): | |||||
def _load_config(self, config): | def _load_config(self, config): | ||||
self._extensions = config.get('hello_video', 'extensions') \ | self._extensions = config.get('hello_video', 'extensions') \ | ||||
.translate(None, ' \t\r\n.') \ | |||||
.translate(str.maketrans('', '', ' \t\r\n.')) \ | |||||
.split(',') | .split(',') | ||||
def supported_extensions(self): | def supported_extensions(self): | ||||
@@ -44,7 +44,7 @@ class HelloVideoPlayer(object): | |||||
self._process.poll() | self._process.poll() | ||||
return self._process.returncode is None | return self._process.returncode is None | ||||
def stop(self, block_timeout_sec=None): | |||||
def stop(self, block_timeout_sec=0): | |||||
"""Stop the video player. block_timeout_sec is how many seconds to | """Stop the video player. block_timeout_sec is how many seconds to | ||||
block waiting for the player to stop before moving on. | block waiting for the player to stop before moving on. | ||||
""" | """ | ||||
@@ -3,7 +3,7 @@ | |||||
# License: GNU GPLv2, see LICENSE.txt | # License: GNU GPLv2, see LICENSE.txt | ||||
import random | import random | ||||
class Playlist(object): | |||||
class Playlist: | |||||
"""Representation of a playlist of movies.""" | """Representation of a playlist of movies.""" | ||||
def __init__(self, movies, is_random): | def __init__(self, movies, is_random): | ||||
@@ -6,7 +6,7 @@ import subprocess | |||||
import time | import time | ||||
class OMXPlayer(object): | |||||
class OMXPlayer: | |||||
def __init__(self, config): | def __init__(self, config): | ||||
"""Create an instance of a video player that runs omxplayer in the | """Create an instance of a video player that runs omxplayer in the | ||||
@@ -17,7 +17,7 @@ class OMXPlayer(object): | |||||
def _load_config(self, config): | def _load_config(self, config): | ||||
self._extensions = config.get('omxplayer', 'extensions') \ | self._extensions = config.get('omxplayer', 'extensions') \ | ||||
.translate(None, ' \t\r\n.') \ | |||||
.translate(str.maketrans('', '', ' \t\r\n.')) \ | |||||
.split(',') | .split(',') | ||||
self._extra_args = config.get('omxplayer', 'extra_args').split() | self._extra_args = config.get('omxplayer', 'extra_args').split() | ||||
self._sound = config.get('omxplayer', 'sound').lower() | self._sound = config.get('omxplayer', 'sound').lower() | ||||
@@ -51,7 +51,7 @@ class OMXPlayer(object): | |||||
self._process.poll() | self._process.poll() | ||||
return self._process.returncode is None | return self._process.returncode is None | ||||
def stop(self, block_timeout_sec=None): | |||||
def stop(self, block_timeout_sec=0): | |||||
"""Stop the video player. block_timeout_sec is how many seconds to | """Stop the video player. block_timeout_sec is how many seconds to | ||||
block waiting for the player to stop before moving on. | block waiting for the player to stop before moving on. | ||||
""" | """ | ||||
@@ -3,10 +3,10 @@ | |||||
# License: GNU GPLv2, see LICENSE.txt | # License: GNU GPLv2, see LICENSE.txt | ||||
import glob | import glob | ||||
from usb_drive_mounter import USBDriveMounter | |||||
from .usb_drive_mounter import USBDriveMounter | |||||
class USBDriveReader(object): | |||||
class USBDriveReader: | |||||
def __init__(self, config): | def __init__(self, config): | ||||
"""Create an instance of a file reader that uses the USB drive mounter | """Create an instance of a file reader that uses the USB drive mounter | ||||
@@ -8,7 +8,7 @@ import time | |||||
import pyudev | import pyudev | ||||
class USBDriveMounter(object): | |||||
class USBDriveMounter: | |||||
"""Service for automatically mounting attached USB drives.""" | """Service for automatically mounting attached USB drives.""" | ||||
def __init__(self, root='/mnt/usbdrive', readonly=False): | def __init__(self, root='/mnt/usbdrive', readonly=False): | ||||
@@ -72,9 +72,9 @@ if __name__ == '__main__': | |||||
drive_mounter = USBDriveMounter(readonly=True) | drive_mounter = USBDriveMounter(readonly=True) | ||||
drive_mounter.mount_all() | drive_mounter.mount_all() | ||||
drive_mounter.start_monitor() | drive_mounter.start_monitor() | ||||
print 'Listening for USB drive changes (press Ctrl-C to quite)...' | |||||
print ('Listening for USB drive changes (press Ctrl-C to quit)...') | |||||
while True: | while True: | ||||
if drive_mounter.poll_changes(): | if drive_mounter.poll_changes(): | ||||
print 'USB drives changed!' | |||||
print ('USB drives changed!') | |||||
drive_mounter.mount_all() | drive_mounter.mount_all() | ||||
time.sleep(0) | time.sleep(0) |
@@ -1,7 +1,8 @@ | |||||
# Copyright 2015 Adafruit Industries. | # Copyright 2015 Adafruit Industries. | ||||
# Author: Tony DiCola | # Author: Tony DiCola | ||||
# License: GNU GPLv2, see LICENSE.txt | # License: GNU GPLv2, see LICENSE.txt | ||||
import ConfigParser | |||||
import configparser | |||||
import importlib | import importlib | ||||
import os | import os | ||||
import re | import re | ||||
@@ -11,7 +12,7 @@ import time | |||||
import pygame | import pygame | ||||
from model import Playlist | |||||
from .model import Playlist | |||||
# Basic video looper architecure: | # Basic video looper architecure: | ||||
@@ -37,14 +38,14 @@ from model import Playlist | |||||
# - Future file readers and video players can be provided and referenced in the | # - Future file readers and video players can be provided and referenced in the | ||||
# config to extend the video player use to read from different file sources | # config to extend the video player use to read from different file sources | ||||
# or use different video players. | # or use different video players. | ||||
class VideoLooper(object): | |||||
class VideoLooper: | |||||
def __init__(self, config_path): | def __init__(self, config_path): | ||||
"""Create an instance of the main video looper application class. Must | """Create an instance of the main video looper application class. Must | ||||
pass path to a valid video looper ini configuration file. | pass path to a valid video looper ini configuration file. | ||||
""" | """ | ||||
# Load the configuration. | # Load the configuration. | ||||
self._config = ConfigParser.SafeConfigParser() | |||||
self._config = configparser.ConfigParser() | |||||
if len(self._config.read(config_path)) == 0: | if len(self._config.read(config_path)) == 0: | ||||
raise RuntimeError('Failed to find configuration file at {0}, is the application properly installed?'.format(config_path)) | raise RuntimeError('Failed to find configuration file at {0}, is the application properly installed?'.format(config_path)) | ||||
self._console_output = self._config.getboolean('video_looper', 'console_output') | self._console_output = self._config.getboolean('video_looper', 'console_output') | ||||
@@ -57,12 +58,12 @@ class VideoLooper(object): | |||||
self._keyboard_control = self._config.getboolean('video_looper', 'keyboard_control') | self._keyboard_control = self._config.getboolean('video_looper', 'keyboard_control') | ||||
# Parse string of 3 comma separated values like "255, 255, 255" into | # Parse string of 3 comma separated values like "255, 255, 255" into | ||||
# list of ints for colors. | # list of ints for colors. | ||||
self._bgcolor = map(int, self._config.get('video_looper', 'bgcolor') \ | |||||
.translate(None, ',') \ | |||||
.split()) | |||||
self._fgcolor = map(int, self._config.get('video_looper', 'fgcolor') \ | |||||
.translate(None, ',') \ | |||||
.split()) | |||||
self._bgcolor = list(map(int, self._config.get('video_looper', 'bgcolor') | |||||
.translate(str.maketrans('','', ',')) | |||||
.split())) | |||||
self._fgcolor = list(map(int, self._config.get('video_looper', 'fgcolor') | |||||
.translate(str.maketrans('','', ',')) | |||||
.split())) | |||||
# Load sound volume file name value | # Load sound volume file name value | ||||
self._sound_vol_file = self._config.get('omxplayer', 'sound_vol_file'); | self._sound_vol_file = self._config.get('omxplayer', 'sound_vol_file'); | ||||
# default value to 0 millibels (omxplayer) | # default value to 0 millibels (omxplayer) | ||||
@@ -1,332 +0,0 @@ | |||||
#!/usr/bin/env python | |||||
"""Bootstrap setuptools installation | |||||
To use setuptools in your package's setup.py, include this | |||||
file in the same directory and add this to the top of your setup.py:: | |||||
from ez_setup import use_setuptools | |||||
use_setuptools() | |||||
To require a specific version of setuptools, set a download | |||||
mirror, or use an alternate download directory, simply supply | |||||
the appropriate options to ``use_setuptools()``. | |||||
This file can also be run as a script to install or upgrade setuptools. | |||||
""" | |||||
import os | |||||
import shutil | |||||
import sys | |||||
import tempfile | |||||
import zipfile | |||||
import optparse | |||||
import subprocess | |||||
import platform | |||||
import textwrap | |||||
import contextlib | |||||
from distutils import log | |||||
try: | |||||
from site import USER_SITE | |||||
except ImportError: | |||||
USER_SITE = None | |||||
DEFAULT_VERSION = "3.5.1" | |||||
DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/" | |||||
def _python_cmd(*args): | |||||
""" | |||||
Return True if the command succeeded. | |||||
""" | |||||
args = (sys.executable,) + args | |||||
return subprocess.call(args) == 0 | |||||
def _install(archive_filename, install_args=()): | |||||
with archive_context(archive_filename): | |||||
# installing | |||||
log.warn('Installing Setuptools') | |||||
if not _python_cmd('setup.py', 'install', *install_args): | |||||
log.warn('Something went wrong during the installation.') | |||||
log.warn('See the error message above.') | |||||
# exitcode will be 2 | |||||
return 2 | |||||
def _build_egg(egg, archive_filename, to_dir): | |||||
with archive_context(archive_filename): | |||||
# building an egg | |||||
log.warn('Building a Setuptools egg in %s', to_dir) | |||||
_python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) | |||||
# returning the result | |||||
log.warn(egg) | |||||
if not os.path.exists(egg): | |||||
raise IOError('Could not build the egg.') | |||||
def get_zip_class(): | |||||
""" | |||||
Supplement ZipFile class to support context manager for Python 2.6 | |||||
""" | |||||
class ContextualZipFile(zipfile.ZipFile): | |||||
def __enter__(self): | |||||
return self | |||||
def __exit__(self, type, value, traceback): | |||||
self.close | |||||
return zipfile.ZipFile if hasattr(zipfile.ZipFile, '__exit__') else \ | |||||
ContextualZipFile | |||||
@contextlib.contextmanager | |||||
def archive_context(filename): | |||||
# extracting the archive | |||||
tmpdir = tempfile.mkdtemp() | |||||
log.warn('Extracting in %s', tmpdir) | |||||
old_wd = os.getcwd() | |||||
try: | |||||
os.chdir(tmpdir) | |||||
with get_zip_class()(filename) as archive: | |||||
archive.extractall() | |||||
# going in the directory | |||||
subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) | |||||
os.chdir(subdir) | |||||
log.warn('Now working in %s', subdir) | |||||
yield | |||||
finally: | |||||
os.chdir(old_wd) | |||||
shutil.rmtree(tmpdir) | |||||
def _do_download(version, download_base, to_dir, download_delay): | |||||
egg = os.path.join(to_dir, 'setuptools-%s-py%d.%d.egg' | |||||
% (version, sys.version_info[0], sys.version_info[1])) | |||||
if not os.path.exists(egg): | |||||
archive = download_setuptools(version, download_base, | |||||
to_dir, download_delay) | |||||
_build_egg(egg, archive, to_dir) | |||||
sys.path.insert(0, egg) | |||||
# Remove previously-imported pkg_resources if present (see | |||||
# https://bitbucket.org/pypa/setuptools/pull-request/7/ for details). | |||||
if 'pkg_resources' in sys.modules: | |||||
del sys.modules['pkg_resources'] | |||||
import setuptools | |||||
setuptools.bootstrap_install_from = egg | |||||
def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, | |||||
to_dir=os.curdir, download_delay=15): | |||||
to_dir = os.path.abspath(to_dir) | |||||
rep_modules = 'pkg_resources', 'setuptools' | |||||
imported = set(sys.modules).intersection(rep_modules) | |||||
try: | |||||
import pkg_resources | |||||
except ImportError: | |||||
return _do_download(version, download_base, to_dir, download_delay) | |||||
try: | |||||
pkg_resources.require("setuptools>=" + version) | |||||
return | |||||
except pkg_resources.DistributionNotFound: | |||||
return _do_download(version, download_base, to_dir, download_delay) | |||||
except pkg_resources.VersionConflict as VC_err: | |||||
if imported: | |||||
msg = textwrap.dedent(""" | |||||
The required version of setuptools (>={version}) is not available, | |||||
and can't be installed while this script is running. Please | |||||
install a more recent version first, using | |||||
'easy_install -U setuptools'. | |||||
(Currently using {VC_err.args[0]!r}) | |||||
""").format(VC_err=VC_err, version=version) | |||||
sys.stderr.write(msg) | |||||
sys.exit(2) | |||||
# otherwise, reload ok | |||||
del pkg_resources, sys.modules['pkg_resources'] | |||||
return _do_download(version, download_base, to_dir, download_delay) | |||||
def _clean_check(cmd, target): | |||||
""" | |||||
Run the command to download target. If the command fails, clean up before | |||||
re-raising the error. | |||||
""" | |||||
try: | |||||
subprocess.check_call(cmd) | |||||
except subprocess.CalledProcessError: | |||||
if os.access(target, os.F_OK): | |||||
os.unlink(target) | |||||
raise | |||||
def download_file_powershell(url, target): | |||||
""" | |||||
Download the file at url to target using Powershell (which will validate | |||||
trust). Raise an exception if the command cannot complete. | |||||
""" | |||||
target = os.path.abspath(target) | |||||
cmd = [ | |||||
'powershell', | |||||
'-Command', | |||||
"(new-object System.Net.WebClient).DownloadFile(%(url)r, %(target)r)" % vars(), | |||||
] | |||||
_clean_check(cmd, target) | |||||
def has_powershell(): | |||||
if platform.system() != 'Windows': | |||||
return False | |||||
cmd = ['powershell', '-Command', 'echo test'] | |||||
devnull = open(os.path.devnull, 'wb') | |||||
try: | |||||
try: | |||||
subprocess.check_call(cmd, stdout=devnull, stderr=devnull) | |||||
except Exception: | |||||
return False | |||||
finally: | |||||
devnull.close() | |||||
return True | |||||
download_file_powershell.viable = has_powershell | |||||
def download_file_curl(url, target): | |||||
cmd = ['curl', url, '--silent', '--output', target] | |||||
_clean_check(cmd, target) | |||||
def has_curl(): | |||||
cmd = ['curl', '--version'] | |||||
devnull = open(os.path.devnull, 'wb') | |||||
try: | |||||
try: | |||||
subprocess.check_call(cmd, stdout=devnull, stderr=devnull) | |||||
except Exception: | |||||
return False | |||||
finally: | |||||
devnull.close() | |||||
return True | |||||
download_file_curl.viable = has_curl | |||||
def download_file_wget(url, target): | |||||
cmd = ['wget', url, '--quiet', '--output-document', target] | |||||
_clean_check(cmd, target) | |||||
def has_wget(): | |||||
cmd = ['wget', '--version'] | |||||
devnull = open(os.path.devnull, 'wb') | |||||
try: | |||||
try: | |||||
subprocess.check_call(cmd, stdout=devnull, stderr=devnull) | |||||
except Exception: | |||||
return False | |||||
finally: | |||||
devnull.close() | |||||
return True | |||||
download_file_wget.viable = has_wget | |||||
def download_file_insecure(url, target): | |||||
""" | |||||
Use Python to download the file, even though it cannot authenticate the | |||||
connection. | |||||
""" | |||||
try: | |||||
from urllib.request import urlopen | |||||
except ImportError: | |||||
from urllib2 import urlopen | |||||
src = dst = None | |||||
try: | |||||
src = urlopen(url) | |||||
# Read/write all in one block, so we don't create a corrupt file | |||||
# if the download is interrupted. | |||||
data = src.read() | |||||
dst = open(target, "wb") | |||||
dst.write(data) | |||||
finally: | |||||
if src: | |||||
src.close() | |||||
if dst: | |||||
dst.close() | |||||
download_file_insecure.viable = lambda: True | |||||
def get_best_downloader(): | |||||
downloaders = [ | |||||
download_file_powershell, | |||||
download_file_curl, | |||||
download_file_wget, | |||||
download_file_insecure, | |||||
] | |||||
for dl in downloaders: | |||||
if dl.viable(): | |||||
return dl | |||||
def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, | |||||
to_dir=os.curdir, delay=15, downloader_factory=get_best_downloader): | |||||
""" | |||||
Download setuptools from a specified location and return its filename | |||||
`version` should be a valid setuptools version number that is available | |||||
as an egg for download under the `download_base` URL (which should end | |||||
with a '/'). `to_dir` is the directory where the egg will be downloaded. | |||||
`delay` is the number of seconds to pause before an actual download | |||||
attempt. | |||||
``downloader_factory`` should be a function taking no arguments and | |||||
returning a function for downloading a URL to a target. | |||||
""" | |||||
# making sure we use the absolute path | |||||
to_dir = os.path.abspath(to_dir) | |||||
zip_name = "setuptools-%s.zip" % version | |||||
url = download_base + zip_name | |||||
saveto = os.path.join(to_dir, zip_name) | |||||
if not os.path.exists(saveto): # Avoid repeated downloads | |||||
log.warn("Downloading %s", url) | |||||
downloader = downloader_factory() | |||||
downloader(url, saveto) | |||||
return os.path.realpath(saveto) | |||||
def _build_install_args(options): | |||||
""" | |||||
Build the arguments to 'python setup.py install' on the setuptools package | |||||
""" | |||||
return ['--user'] if options.user_install else [] | |||||
def _parse_args(): | |||||
""" | |||||
Parse the command line for options | |||||
""" | |||||
parser = optparse.OptionParser() | |||||
parser.add_option( | |||||
'--user', dest='user_install', action='store_true', default=False, | |||||
help='install in user site package (requires Python 2.6 or later)') | |||||
parser.add_option( | |||||
'--download-base', dest='download_base', metavar="URL", | |||||
default=DEFAULT_URL, | |||||
help='alternative URL from where to download the setuptools package') | |||||
parser.add_option( | |||||
'--insecure', dest='downloader_factory', action='store_const', | |||||
const=lambda: download_file_insecure, default=get_best_downloader, | |||||
help='Use internal, non-validating downloader' | |||||
) | |||||
parser.add_option( | |||||
'--version', help="Specify which version to download", | |||||
default=DEFAULT_VERSION, | |||||
) | |||||
options, args = parser.parse_args() | |||||
# positional arguments are ignored | |||||
return options | |||||
def main(): | |||||
"""Install or upgrade setuptools and EasyInstall""" | |||||
options = _parse_args() | |||||
archive = download_setuptools( | |||||
version=options.version, | |||||
download_base=options.download_base, | |||||
downloader_factory=options.downloader_factory, | |||||
) | |||||
return _install(archive, _build_install_args(options)) | |||||
if __name__ == '__main__': | |||||
sys.exit(main()) |
@@ -11,8 +11,7 @@ fi | |||||
echo "Installing dependencies..." | echo "Installing dependencies..." | ||||
echo "==========================" | echo "==========================" | ||||
apt-get update | |||||
apt-get -y install build-essential python-dev python-pip python-pygame supervisor git omxplayer | |||||
apt update && apt -y install git build-essential python3-dev python3 python3-pip python3-pygame supervisor omxplayer | |||||
echo "Installing hello_video..." | echo "Installing hello_video..." | ||||
echo "=========================" | echo "=========================" | ||||
@@ -27,7 +26,8 @@ rm -rf pi_hello_video | |||||
echo "Installing video_looper program..." | echo "Installing video_looper program..." | ||||
echo "==================================" | echo "==================================" | ||||
mkdir -p /mnt/usbdrive0 # This is very important if you put your system in readonly after | mkdir -p /mnt/usbdrive0 # This is very important if you put your system in readonly after | ||||
python setup.py install --force | |||||
pip3 install setuptools | |||||
python3 setup.py install --force | |||||
cp video_looper.ini /boot/video_looper.ini | cp video_looper.ini /boot/video_looper.ini | ||||
echo "Configuring video_looper to run on start..." | echo "Configuring video_looper to run on start..." | ||||
@@ -1,5 +1,3 @@ | |||||
from ez_setup import use_setuptools | |||||
use_setuptools() | |||||
from setuptools import setup, find_packages | from setuptools import setup, find_packages | ||||
setup(name = 'Adafruit_Video_Looper', | setup(name = 'Adafruit_Video_Looper', | ||||
@@ -1,7 +1,7 @@ | |||||
# Supervisord configuration to run video looper at boot and | # Supervisord configuration to run video looper at boot and | ||||
# ensure it runs continuously. | # ensure it runs continuously. | ||||
[program:video_looper] | [program:video_looper] | ||||
command=python -u -m Adafruit_Video_Looper.video_looper | |||||
command=python3 -u -m Adafruit_Video_Looper.video_looper | |||||
autostart=true | autostart=true | ||||
autorestart=unexpected | autorestart=unexpected | ||||
startsecs=5 | startsecs=5 |