Skip to content

Commit

Permalink
feat: Add user selectable CC device
Browse files Browse the repository at this point in the history
Merge pull request skorokithakis#9 from theychx/add-device-choice
  • Loading branch information
skorokithakis authored Dec 11, 2016
2 parents f0a0415 + 229586a commit 5cb506f
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 51 deletions.
88 changes: 60 additions & 28 deletions catt/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,29 @@ def get_local_ip(cc_host):
return s.getsockname()[0]


class Device(object):
def __init__(self, name):
self.name = name


pass_device = click.make_pass_decorator(Device)


@click.group()
@click.option("--delete-cache", is_flag=True, help="Empty the Chromecast "
"discovery cache. Specify this if you're having errors connecting to "
"the Chromecast.")
def cli(delete_cache):
@click.option("--delete-cache", is_flag=True, help="Empty the Chromecast discovery cache.")
@click.option("-d", "--device", metavar="NAME", help="Select Chromecast device.")
@click.pass_context
def cli(ctx, delete_cache, device):
if delete_cache:
Cache().clear()
ctx.obj = Device(device)


@cli.command(short_help="Send a video to a Chromecast for playing.")
@click.argument("video_url")
def cast(video_url):
cast = CastController()
@pass_device
def cast(device, video_url):
cast = CastController(device.name)
cc_name = cast.cast.device.friendly_name

if "://" not in video_url:
Expand Down Expand Up @@ -58,62 +68,84 @@ def cast(video_url):


@cli.command(short_help="Pause a video.")
def pause():
CastController().pause()
@pass_device
def pause(device):
cast = CastController(device.name)
cast.pause()


@cli.command(short_help="Resume a video after it has been paused.")
def play():
CastController().play()
@pass_device
def play(device):
cast = CastController(device.name)
cast.play()


@cli.command(short_help="Stop playing.")
def stop():
CastController().kill()
@pass_device
def stop(device):
cast = CastController(device.name)
cast.kill()


@cli.command(short_help="Rewind a video by SECS seconds.")
@click.argument("seconds", type=click.INT, required=False, default=30, metavar="SECS")
def rewind(seconds):
CastController().rewind(seconds)
@pass_device
def rewind(device, seconds):
cast = CastController(device.name)
cast.rewind(seconds)


@cli.command(short_help="Fastforward a video by SECS seconds.")
@click.argument("seconds", type=click.INT, required=False, default=30, metavar="SECS")
def ffwd(seconds):
CastController().ffwd(seconds)
@pass_device
def ffwd(device, seconds):
cast = CastController(device.name)
cast.ffwd(seconds)


@cli.command(short_help="Seek the video to SECS seconds.")
@click.argument("seconds", type=click.INT, metavar="SECS")
def seek(seconds):
CastController().seek(seconds)
@pass_device
def seek(device, seconds):
cast = CastController(device.name)
cast.seek(seconds)


@cli.command(short_help="Set the volume to LVL [0-1].")
@click.argument("level", type=click.FLOAT, required=False, default=0.5, metavar="LVL")
def volume(level):
CastController().volume(level)
@pass_device
def volume(device, level):
cast = CastController(device.name)
cast.volume(level)


@cli.command(short_help="Turn up volume by an 0.1 increment.")
def volumeup():
CastController().volumeup()
@pass_device
def volumeup(device):
cast = CastController(device.name)
cast.volumeup()


@cli.command(short_help="Turn down volume by an 0.1 increment.")
def volumedown():
CastController().volumedown()
@pass_device
def volumedown(device):
cast = CastController(device.name)
cast.volumedown()


@cli.command(short_help="Show some information about the currently-playing video.")
def status():
CastController().status()
@pass_device
def status(device):
cast = CastController(device.name)
cast.status()


@cli.command(short_help="Show complete information about the currently-playing video.")
def info():
CastController().info()
@pass_device
def info(device):
cast = CastController(device.name)
cast.info()


if __name__ == "__main__":
Expand Down
84 changes: 64 additions & 20 deletions catt/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import pychromecast
import shutil
import tempfile
from click import echo
import json
from click import echo, ClickException


def get_stream_info(video_url):
Expand All @@ -22,29 +23,64 @@ def get_stream_info(video_url):
return stream_info


class ChromecastDeviceError(ClickException):
pass


class Cache:
def __init__(self, cache_dir=os.path.join(tempfile.gettempdir(), "catt_cache/")):
def __init__(self, duration=3 * 24 * 3600,
cache_dir=os.path.join(tempfile.gettempdir(), "catt_cache")):
self.cache_dir = cache_dir
try:
os.mkdir(cache_dir)
except:
pass

def _get_cache_filename(self, key):
return os.path.join(self.cache_dir, key)
self.cache_filename = os.path.join(cache_dir, "chromecast_hosts")

def get(self, key, duration):
cache_filename = self._get_cache_filename(key)
if os.path.exists(cache_filename):
if os.path.getctime(cache_filename) + duration > time.time():
return open(cache_filename).read()
else:
os.remove(cache_filename)
if os.path.exists(self.cache_filename):
if os.path.getctime(self.cache_filename) + duration < time.time():
self._initialize_cache()
else:
self._initialize_cache()

def _initialize_cache(self):
data = {}
devices = pychromecast.get_chromecasts()
for device in devices:
data[device.name] = device.host
self._write_cache(data)

def _read_cache(self):
with open(self.cache_filename, "r") as cache:
return json.load(cache)

def _write_cache(self, data):
with open(self.cache_filename, "w") as cache:
json.dump(data, cache)

def get(self, name):
data = self._read_cache()

# In the case that cache has been initialized with no cc's on the
# network, we need to ensure auto-discovery.
if not data:
return None
# When the user does not specify a device, we need to make an attempt
# to consistently return the same IP, thus the alphabetical sorting.
if not name:
devices = list(data.keys())
devices.sort()
return data[devices[0]]
try:
return data[name]
except KeyError:
return None

def set(self, key, value):
open(self._get_cache_filename(key), "w").write(value)
def set(self, name, value):
data = self._read_cache()
data[name] = value
self._write_cache(data)

def clear(self):
try:
Expand All @@ -54,15 +90,23 @@ def clear(self):


class CastController:
def __init__(self):
def __init__(self, device_name):
cache = Cache()
cached_ip = cache.get(device_name)

try:
if not cached_ip:
raise ValueError
self.cast = pychromecast.Chromecast(cached_ip)
except (pychromecast.error.ChromecastConnectionError, ValueError):
if device_name:
self.cast = pychromecast.get_chromecast(friendly_name=device_name)
else:
self.cast = pychromecast.get_chromecast()
if not self.cast:
raise ChromecastDeviceError("Device not found.")
cache.set(self.cast.name, self.cast.host)

cached_chromecast = cache.get("chromecast_host", 3 * 24 * 3600)
if cached_chromecast:
self.cast = pychromecast.Chromecast(cached_chromecast)
else:
self.cast = pychromecast.get_chromecast()
cache.set("chromecast_host", self.cast.host)
time.sleep(0.2)

def play_media(self, url, content_type="video/mp4"):
Expand Down
10 changes: 7 additions & 3 deletions tests/test_catt.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@ def test_get_stream_info(self):
def test_cache(self):
cache = Cache()
cache.set("key", "value")
self.assertEqual(cache.get("key", 1), "value")
self.assertEqual(cache.get("key"), "value")

time.sleep(1.2)
self.assertIsNone(cache.get("key", 1))
cache = Cache(duration=1)
self.assertEqual(cache.get("key"), None)

cache.set("key", "value")
cache.clear()
self.assertIsNone(cache.get("key", 100))
cache = Cache()
self.assertEqual(cache.get("key"), None)
cache.clear()


if __name__ == '__main__':
Expand Down

0 comments on commit 5cb506f

Please sign in to comment.