#!/usr/bin/python3 import argparse import collections import json import os import time from threading import Thread from datetime import datetime import ox import mpv import logging logger = logging.getLogger('p_for_power') SYNC_TOLERANCE = 0.05 SYNC_GRACE_TIME = 5 SYNC_JUMP_AHEAD = 1 PORT = 9067 DEBUG = False CONFIG = { "vf": None, "sync_group": None, } def hide_gnome_overview(): import dbus bus = dbus.SessionBus() shell = bus.get_object('org.gnome.Shell', '/org/gnome/Shell') props = dbus.Interface(shell, 'org.freedesktop.DBus.Properties') props.Set('org.gnome.Shell', 'OverviewActive', False) def mpv_log(loglevel, component, message): logger.info('[{}] {}: {}'.format(loglevel, component, message)) class Main: playlist_current_pos = -1 time_pos = -1 class Sync(Thread): active = True is_main = True is_paused = False ready = False destination = "255.255.255.255" reload_check = None _pos = None _tick = 0 need_to_sync = False loops = [] def __init__(self, *args, **kwargs): self.is_main = kwargs.get('mode', 'main') == 'main' self.start_at_hour = kwargs.get("hour", False) self.main = Main() if kwargs.get("music"): music = mpv.MPV( log_handler=mpv_log, input_default_bindings=False, input_vo_keyboard=False, ) music.loop_file = True music.play("/srv/p_for_power/render/music-5.1.mp4") self.loops.append(music) if kwargs.get("forest"): forest = mpv.MPV( log_handler=mpv_log, input_default_bindings=False, input_vo_keyboard=False, ) forest.loop_file = True forest.play("/srv/p_for_power/render/forest-5.1.mp4") self.loops.append(forest) self.mpv = mpv.MPV( log_handler=mpv_log, input_default_bindings=True, input_vo_keyboard=True, ) if CONFIG.get("vf"): self.mpv.vf = CONFIG["vf"] self.mpv.observe_property('time-pos', self.time_pos_cb) self.mpv.fullscreen = kwargs.get('fullscreen', False) self.mpv.loop_file = False self.mpv.loop_playlist = True self.mpv.register_key_binding('q', self.q_binding) self.mpv.register_key_binding('s', self.s_binding) self.mpv.register_key_binding('p', self.p_binding) self.mpv.register_key_binding('SPACE', self.space_binding) self.playlist = kwargs['playlist'] self.playlist_mtime = os.stat(self.playlist).st_mtime self.mpv.loadlist(self.playlist) logger.error("loaded paylist: %s", self.playlist) logger.debug("current playlist: %s", json.dumps(self.mpv.playlist, indent=2)) self.deviations = collections.deque(maxlen=10) if self.start_at_hour: self.mpv.pause = True fmt = '%Y-%m-%d %H' now = datetime.now() offset = (now - datetime.strptime(now.strftime(fmt), fmt)).total_seconds() for loop in self.loops: loop.wait_until_playing() loop.seek(offset, 'absolute', 'exact') loop.pause = True position = 0 for idx, item in enumerate(self.mpv.playlist): duration = ox.avinfo(item['filename'])['duration'] if position + duration > offset: pos = offset - position self.mpv.playlist_play_index(idx) self.mpv.pause = False self.mpv.wait_until_playing() self.mpv.seek(pos, 'absolute', 'exact') time.sleep(0.1) break else: position += duration for loop in self.loops: loop.pause = False self.ready = True Thread.__init__(self) self.start() def run(self): while self.active: time.sleep(0.5) self.reload_playlist() if not self.is_paused and self._tick and abs(time.time() - self._tick) > 60: logger.error("player is stuck") self._tick = 0 self.stop() self.mpv.stop() def is_keydown(self, args): if args and args[0] and args[0][0] == 'd': return True return False def q_binding(self, *args): if self.is_keydown(args): self.stop() self.mpv.stop() def space_binding(self, *args): if self.is_keydown(args): if self.mpv.pause: self.p_binding(*args) else: self.s_binding(*args) def s_binding(self, *args): if self.is_keydown(args): self.is_paused = True self.mpv.pause = True for loop in self.loops: loop.pause = True def p_binding(self, *args): if self.is_keydown(args): self.is_paused = False self._tick = 0 self.mpv.pause = False for loop in self.loops: loop.pause = False def stop(self, *args): self.active = False def time_pos_cb(self, pos, *args, **kwargs): self._tick = time.time() if self._pos != self.mpv.playlist_current_pos: self._pos = self.mpv.playlist_current_pos try: track = self.mpv.playlist[self._pos] logger.error("%s %s", datetime.now(), track["filename"]) except: pass def reload_playlist(self): if not self.reload_check: self.reload_check = time.time() if time.time() - self.reload_check > 5: self.reload_check = time.time() playlist_mtime = os.stat(self.playlist).st_mtime if self.playlist_mtime != playlist_mtime: self.playlist_mtime = playlist_mtime #self.mpv.loadlist(self.playlist) with open(self.playlist) as fd: items = fd.read().strip().split('\n') base = os.path.dirname(self.playlist) items = [os.path.join(base, item) for item in items] current_items = self.mpv.playlist_filenames for filename in items: if filename not in current_items: self.mpv.playlist_append(filename) logger.error("add: %s", filename) remove = [] for filename in current_items: if filename not in items: remove.append(filename) for filename in remove: for idx, item in enumerate(self.mpv.playlist): if item["filename"] == filename: logger.error("remove: %s %s", idx, filename) self.mpv.playlist_remove(idx) break for idx, filename in enumerate(items): current_idx = self.mpv.playlist_filenames.index(filename) if idx != current_idx: logger.error("move item %s %s -> %s", filename, current_idx, idx) self.mpv.playlist_move(current_idx, idx) logger.error("reloaded paylist: %s", self.playlist) logger.debug("current playlist: %s", json.dumps(self.mpv.playlist, indent=2)) def main(): parser = argparse.ArgumentParser(description='p_for_power player') parser.add_argument('--mode', help='peer or main', default="main") parser.add_argument('--playlist', default='/srv/p_for_power/render/front.m3u', help="m3u") parser.add_argument('--window', action='store_true', help='run in window', default=False) parser.add_argument('--debug', action='store_true', help='debug', default=False) parser.add_argument('--hour', action='store_true', help='hour', default=False) parser.add_argument('--no-forest', action='store_false', help='disable background forest loop', default=False) parser.add_argument('--no-music', action='store_false', help='disable background music loop', default=False) parser.add_argument('--config', help='config', default=None) args = parser.parse_args() DEBUG = args.debug if DEBUG: log_format = '%(asctime)s:%(levelname)s:%(name)s:%(message)s' logging.basicConfig(level=logging.DEBUG, format=log_format) if args.config: if os.path.exists(args.config): with open(args.config) as fd: CONFIG.update(json.load(fd)) else: logger.error("config file %s does not exist, skipping", args.config) base = os.path.dirname(os.path.abspath(__file__)) #os.chdir(base) player = Sync( mode=args.mode, playlist=args.playlist, fullscreen=not args.window, hour=args.hour, music=not args.no_music, forest=not args.no_forest ) while player.active: try: player.mpv.wait_for_playback() except: break player.stop() if __name__ == "__main__": try: hide_gnome_overview() except: pass main()