From 1b7933187e2a57b973d6f4178a5ff543a3930f38 Mon Sep 17 00:00:00 2001 From: Peder Bergebakken Sundt Date: Mon, 26 Feb 2018 22:55:01 +0100 Subject: [PATCH] Initial commit and design --- .gitignore | 3 + api.py | 68 +++++++++++++++++++++++ config.py | 4 ++ main.py | 142 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 + res/logo.jpg | Bin 0 -> 6848 bytes res/pvv logo.svg | 47 ++++++++++++++++ utils.py | 27 +++++++++ 8 files changed, 293 insertions(+) create mode 100644 .gitignore create mode 100644 api.py create mode 100644 config.py create mode 100755 main.py create mode 100644 requirements.txt create mode 100644 res/logo.jpg create mode 100644 res/pvv logo.svg create mode 100644 utils.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..27476c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/default_config.py +__pycache__/ +*.pyc diff --git a/api.py b/api.py new file mode 100644 index 0000000..5155fc4 --- /dev/null +++ b/api.py @@ -0,0 +1,68 @@ +import requests, urllib +from functools import wraps +from pathlib import Path + +# (TODO)Move to config? +BASE_URL = Path('http://bokhylle.pvv.ntnu.no:8080/api') + +# Exceptions: +class APIError(Exception): pass + +# decorator: +def request_post(func): + @wraps(func) + def new_func(*args, **kwargs): + url, data = func(*args, **kwargs) + response = requests.post(url, data=data) + json = json.loads(response.text) + if "error" not in json or json["error"] != False: + raise APIError(json["error_msg"]) + return json["success"] + return new_func +def request_get(func): + @wraps(func) + def new_func(*args, **kwargs): + url = func(*args, **kwargs) + response = requests.get(url) + json = json.loads(response.text) + if "error" not in json or json["error"] != False: + raise APIError(json["error_msg"]) + return json["value"] + return new_func + +# methods: + +@request_post +def is_playing(path:str): + args = urllib.urlencode(locals()) + return BASE_URL / f"play?{args}", None + +@request_get +def is_playing(): + return BASE_URL / f"play" + +@request_post +def set_playing(play:bool): + args = urllib.urlencode(locals()) + return BASE_URL / f"play?{args}", None + +@request_get +def get_volume(): + return BASE_URL / f"volume" + +@request_post +def set_volume(volume:int):# between 0 and 100 (you may also exceed 100) + args = urllib.urlencode(locals()) + return BASE_URL / f"volume?{args}", None + +@request_get +def get_playlist(): + return BASE_URL / f"playlist" + +@request_post +def playlist_next(): + return BASE_URL / f"playlist/next", None + +@request_post +def playlist_previous(): + return BASE_URL / f"playlist/previous", None diff --git a/config.py b/config.py new file mode 100644 index 0000000..37c448b --- /dev/null +++ b/config.py @@ -0,0 +1,4 @@ +host = "0.0.0.0" +port = 8080 +start_browser = False +multiple_instance = True diff --git a/main.py b/main.py new file mode 100755 index 0000000..aaa411d --- /dev/null +++ b/main.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +import random, os, time, shutil, sys +from threading import Timer +import remi.gui as gui +from remi import start, App +from utils import Namespace + +#globals: +COLOR_BLUE = "rgb(33, 150, 243)" +COLOR_BLUE_SHADOW = "rgba(33, 150, 243, 0.75)" + +class MyApp(App): + def __init__(self, *args): + res_path = os.path.join(os.path.dirname(__file__), 'res') + super(MyApp, self).__init__(*args, static_file_path=res_path) + + def main(self): + container = gui.VBox(width=512) + container.style["margin-left"] = "auto" + container.style["margin-right"] = "auto" + + #logo: + container.append(gui.Image('/res/logo.jpg', width=512)) + + #playback controls + playbackContainer = gui.HBox()#; container.append(playbackContainer) + + self.playback = Namespace() + for i in ("previous", "play", "next"): + button = gui.Button(i.capitalize(), margin="5px") + setattr(self.playback, i, button) + playbackContainer.append(button) + button.set_on_click_listener(getattr(self,'playback_%s' % i)) + + self.playback.playing = gui.Label("Now playing: None") + self.playback.slider = gui.Slider(0, 0, 100, 1, width="85%", height=20, margin='10px') + + container.append(self.playback.playing) + container.append(playbackContainer) + container.append(self.playback.slider) + + #playlist + self.playlist = Namespace() + self.playlist.table = gui.Table(width="100%", margin="10px") + self.playlist.table.append_from_list([['#', 'Name', "length"]], fill_title=True) + + container.append(self.playlist.table) + + self.playlist.queue = []#[i] = [source, name, length] + + #input + container.append(gui.Label("Add songs:")) + inputContainer = gui.HBox(width=512) + self.input = Namespace() + self.input.field = gui.TextInput(single_line=True, height="20px", margin="5px") + self.input.field.style["border"] = "1px solid %s" % COLOR_BLUE + self.input.field.style["box-shadow"] = "0px 0px 5px 0px %s" % COLOR_BLUE_SHADOW + self.input.submit = gui.Button("Submit!", margin="5px") + self.input.field.set_on_enter_listener(self.input_submit) + self.input.submit.set_on_click_listener(self.input_submit) + + inputContainer.append(self.input.field) + inputContainer.append(self.input.submit) + container.append(inputContainer) + + #return the container + self.mainLoop() + return container + def mainLoop(self): + #self.playback.slider.get_value() + + self.playback_update() + + + self.playlist.table.empty(keep_title=True) + self.playlist.table.append_from_list(self.playlist_update()) + + Timer(0.7, self.mainLoop).start() + + # events: + def playback_previous(self, widget): pass + def playback_play(self, widget):# toggle playblack + pass + def playback_next(self, widget): + source, name, length = self.playlist.queue.pop(0) + + pass + def input_submit(self, widget, value=None): + if not value: + value = self.input.field.get_text() + self.input.field.set_text("") + + title, length = get_youtube_metadata(value) + + self.playlist.queue.append([value, title, length]) + + # playback steps: + def playback_update(self): + #talk to mpv, see wether the song is being played still + if 0:#if done: + self.playback_next() + self.playback.slider.set_value(0) + else: + self.playback.slider.set_value(100) + + return + def playlist_update(self): + #out = [['#', 'Name', "length"]] + out = [] + for i, (source, name, length) in enumerate(self.playlist.queue): + out.append([str(i+1), name, length]) + + return out + + +# config must be a object with the attributes:: +# config.host: str +# config.port: str +# config.start_browser: bool +# config.multiple_instance: bool +def main(config): + assert hasattr(config, "host") + assert hasattr(config, "port") + assert hasattr(config, "start_browser") + assert hasattr(config, "multiple_instance") + + # start the webserver: + start( + MyApp, + title = "Gregorz", + address = config.host, + port = config.port, + start_browser = config.start_browser, + multiple_instance = config.multiple_instance, + enable_file_cache = True + ) + +if __name__ == "__main__": + if not os.path.exists("config.py"): + shutil.copy("default_config.py", "config.py") + import config + main(config) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..dcd67fa --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +youtube-dl +https://github.com/dddomodossola/remi/archive/v1.0.tar.gz diff --git a/res/logo.jpg b/res/logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..19373e2dcd3ebc38bd99ca319700d59efe0ae624 GIT binary patch literal 6848 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PVV + + diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..27751ee --- /dev/null +++ b/utils.py @@ -0,0 +1,27 @@ +import threading +import youtube_dl + +class Namespace(object): pass + +def get_youtube_metadata(url, ydl = youtube_dl.YoutubeDL()): + #todo: check if url is valid + + #todo, stop it from doung the whole playlist + resp = ydl.extract_info(url, download=False) + #print resp.keys() + + title = resp.get('title') + length = resp.get('duration') + + #print( title, "%i:%.2i" % (length//60, length%60)) + return title, "%i:%.2i" % (length//60, length%60) + +# decorator: +def call_as_thread(func): + def new_func(*args, **kwargs): + threading.Thread( + target = func, + args = args, + kwargs = kwargs + ).start() + return new_func