From ac4018e3ad9037162632585b48060c0cdbcc9be4 Mon Sep 17 00:00:00 2001 From: ben Date: Mon, 15 May 2023 13:15:12 +0000 Subject: [PATCH] recent changes (!3) blink1 as a config package moosetv schedule airconnect latency audiobookshelf fix metadata cron grow lights automations moosetv in task how does bluetooth tracker work owntone metadata scan daily pulseaudio packages fix hass git sync script fix script adjust trusted_networks add map to zwavejs nginx config trying to fix 'websocket disconnected' errors, and killed stupid serviceworker cache tweaking metadata script pulling the metadata from abs, the notes script htat isnt done hass nginx tls settings working on adaptive lighting add websocket api to config use /24 wip: glados integrating with hass fix binary sensor new chromecast version again config cleanup wip: glados integrating with hass grow_lights_1_cron_send_state Co-authored-by: Ben Kristinsson Reviewed-on: https://git.sudo.is/b/infra/pulls/3 --- roles/airconnect/defaults/main.yml | 1 + roles/airconnect/templates/airupnp.xml.j2 | 2 +- .../audiobookshelf/files/fix-podcast-date.py | 196 +++++++---- .../audiobookshelf/files/fix-podcast-date2.py | 328 ++++++++++++++++++ roles/audiobookshelf/files/pods.py | 219 ++++++++++++ roles/audiobookshelf/tasks/audiobookshelf.yml | 38 ++ .../templates/02-audiobookshelf.conf.j2 | 33 +- .../templates/audiobookshelf-cron.j2 | 6 + roles/hass/defaults/main.yml | 1 - .../files/packages/adaptive_lighting.yaml | 141 ++++++++ roles/hass/files/packages/grow_lights.yaml | 43 ++- roles/hass/files/packages/moosetv.yaml | 8 + roles/hass/handlers/main.yml | 5 + roles/hass/tasks/hass.yml | 11 +- roles/hass/templates/01-hass.conf.j2 | 12 +- roles/hass/templates/01-zwavejs.conf.j2 | 5 + roles/hass/templates/blink1.yaml.j2 | 71 ---- roles/hass/templates/configuration.yaml.j2 | 60 +--- roles/hass/templates/git-hass-config.sh.j2 | 40 ++- roles/hass/templates/packages/blink1.yaml.j2 | 111 ++++++ .../templates/packages/glados_tts.yaml.j2 | 23 ++ roles/hass/templates/templates.yaml.j2 | 9 - roles/homeaudio/tasks/homeaudio.yml | 9 + roles/jellyfin/templates/01-jellyfin.j2 | 2 +- roles/owntone/templates/owntone-cron.j2 | 1 + roles/owntone/templates/owntone.conf.j2 | 14 +- roles/wireguard/templates/wg0.conf.j2 | 2 +- 27 files changed, 1149 insertions(+), 242 deletions(-) create mode 100644 roles/audiobookshelf/files/fix-podcast-date2.py create mode 100755 roles/audiobookshelf/files/pods.py create mode 100644 roles/audiobookshelf/templates/audiobookshelf-cron.j2 create mode 100644 roles/hass/files/packages/adaptive_lighting.yaml create mode 100644 roles/hass/files/packages/moosetv.yaml delete mode 100644 roles/hass/templates/blink1.yaml.j2 create mode 100644 roles/hass/templates/packages/blink1.yaml.j2 create mode 100644 roles/hass/templates/packages/glados_tts.yaml.j2 diff --git a/roles/airconnect/defaults/main.yml b/roles/airconnect/defaults/main.yml index 653005a..21605b7 100644 --- a/roles/airconnect/defaults/main.yml +++ b/roles/airconnect/defaults/main.yml @@ -10,6 +10,7 @@ airconnect_group: # for flac: 'flc' airconnect_codec: mp3:320 +airconnect_latency: 1000:2000 airconnect_max_volume: "100" airconnect_upnp: [] diff --git a/roles/airconnect/templates/airupnp.xml.j2 b/roles/airconnect/templates/airupnp.xml.j2 index e45ee46..8ef0f7e 100644 --- a/roles/airconnect/templates/airupnp.xml.j2 +++ b/roles/airconnect/templates/airupnp.xml.j2 @@ -15,7 +15,7 @@ 1 1 - 0:500 + {{ airconnect_latency }} 0 info diff --git a/roles/audiobookshelf/files/fix-podcast-date.py b/roles/audiobookshelf/files/fix-podcast-date.py index b5ac854..e3cbd4a 100755 --- a/roles/audiobookshelf/files/fix-podcast-date.py +++ b/roles/audiobookshelf/files/fix-podcast-date.py @@ -5,13 +5,15 @@ from datetime import datetime import subprocess import json import sys -import shutil import os import time +import eyed3 from loguru import logger import dateutil.parser from dateutil.parser._parser import ParserError +from mutagen.id3 import ID3 + def delete_file(file_path): try: @@ -24,8 +26,7 @@ def delete_file(file_path): def replace_file(src, dest): try: logger.debug(f"src: {src}, dest: {dest}") - delete_file(dest) - shutil.move(src, dest) + os.replace(src, dest) logger.debug(f"replaced '{dest}'") except (PermissionError, OSError, FileNotFoundError) as e: logger.error(e) @@ -61,9 +62,8 @@ def ffmpeg_write_date_tag(podcast_file, out_file, iso_date_str): "-nostdin", "-y", # overwrite output file "-i", podcast_file, - #"-c", "copy", + "-c", "copy", "-metadata", f"date={iso_date_str}", - #"-metadata", f"releasedate={iso_date_str}", out_file ] @@ -80,24 +80,16 @@ def ffmpeg_write_date_tag(podcast_file, out_file, iso_date_str): def eyeD3_write_date_tag(podcast_file, iso_date_str): - - # import eyeD3 ? - - podcast_dir = os.path.basename(podcast_file) - cover_path = os.path.join(podcast_dir, "cover.jpg") - cmd = [ "eyeD3", "--release-date", iso_date_str, - "--orig-release-date", iso_date_str, - "--recording-date", iso_date_str, - # this overwrites 'release date' i think: - #"--release-year", iso_date_str.split("-")[0], - #"--preserve-file-times" + "--to-v2.4", + "--user-text-frame", f"releasedate:{iso_date_str}", + # "--preserve-file-times", + # "--recording-date", iso_date_str, + # "--orig-release-date", iso_date_str, + podcast_file ] - # if os.path.exists(cover_path): - # cmd.extend(["--add-image", f"{cover_path}:FRONT_COVER"]) - cmd.append(podcast_file) logger.debug(" ".join(cmd)) @@ -122,7 +114,6 @@ def get_podcast_name_from_dir(podcast_file): def eyeD3_write_album_tag(podcast_file, podcast_name): # "album" is the name of the podcast - cmd = ["eyeD3", "--album", podcast_name, podcast_file] try: subprocess.run(cmd, capture_output=True, check=True, stdin=None) @@ -132,27 +123,101 @@ def eyeD3_write_album_tag(podcast_file, podcast_name): def parse_iso_date(date_str): + if date_str is None: + return None + try: dt = dateutil.parser.parse(date_str) return dt.date().isoformat() except (ParserError, TypeError) as e: logger.warning(f"invalid date string: '{date_str}'") + return None -def parse_TDAT_tag(tag_tdat): +def parse_TDAT_tag(tag_tdat, tag_tyer): + if tag_tdat is None: + return None + if tag_tyer is None: + return None + if not isinstance(tag_tdat, str) or len(tag_tdat) != 4: + return None + if not isinstance(tag_tyer, str) or len(tag_tyer) != 4: + return None + + + # TDAT is id3 v2.3: DDMM + # TYER is id3 v2.3: YYYY try: - iso_date_str = tag_tdat.split(' ')[0] - return parse_iso_date(iso_date_str) + #iso_date_str = tag_tdat.split(' ')[0] + #return parse_iso_date(iso_date_str) + DD = tag_tdat[0:2] + MM = tag_tdat[2:4] + YYYY = tag_tyer[0:4] + isofmt = f"{YYYY}-{MM}-{DD}" + if is_iso_string(isofmt): + return isofmt + else: + logger.warning(f"invalid TDAT: {tag_tdat} and TYER: {tag_tyer}") + return None except (AttributeError, IndexError) as e: logger.debug(f"invalid 'TDAT' tag: '{tag_tdat}'") return None +def is_iso_string(iso_string): + if iso_string is None: + return False + try: + datetime.fromisoformat(iso_string) + return True + except ValueError: + return False + def get_iso_date_in_file(file_path): tags = ffprobe_get_tags(file_path) + l = [] + + # print(tag_tdat) + # print(tag_tyer) + for item in ["releasedate", "date"]: + parsed = parse_iso_date(tags.get(item)) + if is_iso_string(parsed): + l.append(parsed) + + + tag_tdat = tags.get("TDAT") + tag_tyer = tags.get("TYER") + if len(l) == 0 and (tag_tdat is not None and tag_tyer is not None): + logger.info(f"TDAT: {tag_tdat}") + logger.info(f"TYER: {tag_tyer}") + tdat = parse_TDAT_tag(tag_tdat, tag_tyer) + if is_iso_string(tdat): + l.append(tdat) + + + dates = set(l) + if len(dates) == 0: + logger.error(f"no valid date found for '{file_path}'") + raise SystemExit(3) + + + elif len(dates) == 1: + d = list(dates)[0] + logger.info(f"date found: {d}") + return d + + else: + logger.info(f"multiple dates found: {dates}, picking earliest") + earliest = min([datetime.fromisoformat(a) for a in dates]) + return earliest.isoformat() + + +def get_iso_date_in_file2(file_path): + tags = ffprobe_get_tags(file_path) tag_TDAT = tags.get("TDAT") tag_date = tags.get("date") + #tag_releasedate = tags.get("releasedate") parsed_TDAT = parse_TDAT_tag(tag_TDAT) parsed_date = parse_iso_date(tag_date) @@ -173,22 +238,24 @@ def get_iso_date_in_file(file_path): else: return parsed_TDAT +def st_time_to_iso(st_time): + return datetime.fromtimestamp(st_time).isoformat() + +def show_info(file_path): + statinfo = os.stat(file_path) + + atime = st_time_to_iso(statinfo.st_atime) + mtime = st_time_to_iso(statinfo.st_mtime) + ctime = st_time_to_iso(statinfo.st_ctime) + logger.info(f"atime: {atime}") + logger.info(f"mtime: {mtime}") + logger.info(f"ctime: {ctime}") -def file_dates_are_ok(file_path): - tags = ffprobe_get_tags(file_path) - tag_date = tags.get("date") - try: - dt = datetime.fromisoformat(tag_date) - ts = time.mktime(dt.timetuple()) - os.stat(file_path).st_mtime == ts - except ValueError: - return False def set_utime(file_path, iso_date_str): + # settings access and modified times dt = dateutil.parser.parse(iso_date_str) ts = time.mktime(dt.timetuple()) - # shutil.move(file_path, f"{file_path}.new") - # shutil.move(f"{file_path}.new", file_path) os.utime(file_path, (ts, ts)) try: os.utime(os.path.dirname(file_path), (ts, ts)) @@ -196,16 +263,24 @@ def set_utime(file_path, iso_date_str): pass return dt +def eyed3_dates(podcast_file, date): + a = eyed3.load(podcast_file) + + +def mutagen_dates(podcast_file, date): + id3 = ID3(podcast_file) + print(type(id3)) + def parse_args(): parser = argparse.ArgumentParser() - parser.add_argument("podcast_file") - parser.add_argument("--out-file", default="/tmp/out-{os.getpid()}.mp3") + parser.add_argument("action", choices=["show", "utime-only", "fix-dates", "fix-album-tag"]) + parser.add_argument("--podcast-file") parser.add_argument("--debug", action="store_true") parser.add_argument("--quiet", action="store_true") - parser.add_argument("--ffmpeg", action="store_true") - parser.add_argument("--mtime", action="store_true", help="only set mtime, no mp3 metadata") - parser.add_argument("--fix-album-tag", action="store_true", help="write album tag (podcast name)") + parser.add_argument("--ffmpeg-out-file", default=f"/tmp/out-{os.getpid()}.mp3") + parser.add_argument("--metadata-util", default="eyed3", choices=["mutagen", "eyeD3", "ffmpeg"], type=str.lower) + parser.add_argument("--utime-only", action="store_true", help="only set utime, no mp3 metadata") parser.add_argument("--podcast-name") args = parser.parse_args() @@ -216,7 +291,6 @@ def parse_args(): logger.add(sys.stderr, level="ERROR") else: logger.add(sys.stderr, level="INFO") - return args @@ -224,31 +298,33 @@ def main(): args = parse_args() logger.debug(f"checking: '{os.path.basename(args.podcast_file)}'") - if args.fix_album_tag: + date = get_iso_date_in_file(args.podcast_file) + + if args.action == "show": + show_info(args.podcast_file) + + if args.action == "utime-only": + dt = set_utime(args.podcast_file, date) + logger.info(f"set mtime for '{os.path.basename(args.podcast_file)}' to '{dt.isoformat()}' according to mp3 metadata") + + if args.action == "fix-dates": + if args.metadata_util == "ffmpeg": + ffmpeg_write_date_tag(args.podcast_file, args.out_file, date) + if args.metadata_util == "eyed3": + eyeD3_write_date_tag(args.podcast_file, date) + eyed3_dates(args.podcast_file, date) + if args.metadata_util == "mutagen": + mutagen_dates(args.podcast_file, date) + + set_utime(args.podcast_file, date) + logger.success(f"updated dates (metadata and file attributes) for '{args.podcast_file}' as {date}") + + if args.action == "fix-album-tag": podcast_name = get_podcast_name_from_dir(args.podcast_file) if podcast_name is not None: eyeD3_write_album_tag(args.podcast_file, podcast_name) logger.info(f"set album tag to '{podcast_name}' for '{args.podcast_file}'") - date = get_iso_date_in_file(args.podcast_file) - - if args.mtime: - dt = set_utime(args.podcast_filen, date) - logger.info(f"set mtime for '{os.path.basename(args.podcast_file)}' to '{dt.isoformat()}' according to mp3 metadata") - - elif file_dates_are_ok(args.podcast_file): - logger.info(f"metadata date and filesystem utimes ar ok for {args.podcast_file}', did not modify file") - else: - if args.ffmpeg: - ffmpeg_write_date_tag(args.podcast_file, args.out_file, date) - else: - eyeD3_write_date_tag(args.podcast_file, date) - - set_utime(args.podcast_file, date) - logger.success(f"updated date in '{args.podcast_file}' as {date}") - - - if __name__ == "__main__": main() diff --git a/roles/audiobookshelf/files/fix-podcast-date2.py b/roles/audiobookshelf/files/fix-podcast-date2.py new file mode 100644 index 0000000..c04cbeb --- /dev/null +++ b/roles/audiobookshelf/files/fix-podcast-date2.py @@ -0,0 +1,328 @@ +#!/usr/bin/env python3 + +import argparse +from datetime import datetime +import subprocess +import json +import sys +import shutil +import os +import time + +import eyed3 +from loguru import logger +import dateutil.parser +from dateutil.parser._parser import ParserError +from mutagen.id3 import ID3 + +def delete_file(file_path): + try: + os.remove(file_path) + logger.debug(f"deleted: '{file_path}'") + except FileNotFoundError: + pass + + +def replace_file(src, dest): + try: + logger.debug(f"src: {src}, dest: {dest}") + #delete_file(dest) + #shutil.move(src, dest) + os.replace(src, dest) + + logger.debug(f"replaced '{dest}'") + except (PermissionError, OSError, FileNotFoundError) as e: + logger.error(e) + raise SystemExit(2) + + +def ffprobe_get_tags(file_path): + cmd = [ + "ffprobe", + "-v", "quiet", + file_path, + "-print_format", "json", + "-show_entries", + "stream_tags:format_tags" + ] + try: + p = subprocess.run(cmd, capture_output=True, check=True) + j = json.loads(p.stdout) + return j['format']['tags'] + except subprocess.CalledProcessError as e: + logger.error(f"{cmd[0]} exited with returncode {e.returncode} \n{e.stderr.decode()}") + raise SystemExit(e.returncode) + except KeyError as e: + logger.error(f"key {e} for file '{file_path}' not found in ffprobe stdout: {p.stdout.decode()}") + raise SystemExit(2) + + +def ffmpeg_write_date_tag(podcast_file, out_file, iso_date_str): + delete_file(out_file) + + cmd = [ + "ffmpeg", + "-nostdin", + "-y", # overwrite output file + "-i", podcast_file, + "-c", "copy", + "-metadata", f"date={iso_date_str}", + #"-metadata", f"releasedate={iso_date_str}", + out_file + ] + + try: + p = subprocess.run(cmd, capture_output=True, check=True, stdin=None) + p.check_returncode() + logger.debug(f"output: '{out_file}'") + replace_file(out_file, podcast_file) + except subprocess.CalledProcessError as e: + logger.error(f"{cmd[0]} exited with returncode {e.returncode} \n{e.stderr.decode()}") + raise SystemExit(e.returncode) + finally: + delete_file(out_file) + + +def eyeD3_write_date_tag(podcast_file, iso_date_str): + + # import eyeD3 ? + + podcast_dir = os.path.basename(podcast_file) + cover_path = os.path.join(podcast_dir, "cover.jpg") + + cmd = [ + "eyeD3", + "--release-date", iso_date_str, + "--to-v2.4", + "--user-text-frame", f"releasedate:{iso_date_str}", + #"--preserve-file-times", + # "--recording-date", iso_date_str, + # "--force-update", + podcast_file + #"--orig-release-date", iso_date_str, + + # this overwrites 'release date' i think: + #"--release-year", iso_date_str.split("-")[0], + + ] + # if os.path.exists(cover_path): + # cmd.extend(["--add-image", f"{cover_path}:FRONT_COVER"]) + #cmd.append(podcast_file) + + logger.debug(" ".join(cmd)) + + try: + subprocess.run(cmd, capture_output=True, check=True, stdin=None) + logger.debug(f"updated: '{podcast_file}'") + #logger.info(f"would write date: {iso_date_str}") + except subprocess.CalledProcessError as e: + logger.error(f"{cmd[0]} exited with returncode {e.returncode} \n{e.stderr.decode()}") + raise SystemExit(e.returncode) + + +def get_podcast_name_from_dir(podcast_file): + podcast_dir = os.path.dirname(podcast_file) + if podcast_dir.startswith("/"): + # for now lets just do absolute dirs for names + podcast_name = os.path.basename(podcast_dir) + logger.debug(f"podcast name: {podcast_name}") + return podcast_name + else: + return None + + +def eyeD3_write_album_tag(podcast_file, podcast_name): + # "album" is the name of the podcast + + cmd = ["eyeD3", "--album", podcast_name, podcast_file] + try: + subprocess.run(cmd, capture_output=True, check=True, stdin=None) + except subprocess.CalledProcessError as e: + logger.error(f"{cmd[0]} exited with returncode {e.returncode} \n{e.stderr.decode()}") + raise SystemExit(e.returncode) + + +def parse_iso_date(date_str): + try: + dt = dateutil.parser.parse(date_str) + return dt.date().isoformat() + except (ParserError, TypeError) as e: + logger.warning(f"invalid date string: '{date_str}'") + return None + +def parse_TDAT_tag(tag_tdat): + try: + iso_date_str = tag_tdat.split(' ')[0] + return parse_iso_date(iso_date_str) + except (AttributeError, IndexError) as e: + logger.debug(f"invalid 'TDAT' tag: '{tag_tdat}'") + return None + +def is_iso_string(iso_string): + if iso_string is None: + return False + try: + datetime.fromisoformat(iso_string) + return True + except ValueError: + return False + +# def get_iso_date_in_file(file_path): +# tags = ffprobe_get_tags(file_path): + +# search_tag_names = ["date", "releasedate", "TDAT"] +# tags_data = [tags.get(a) for a in search_tag_names] +# for item in tags_data: + + +# tags_parsed = [parse_iso_date(a) for a in tags_data] + + +# for item in ["date", "TDAT", "releasedate"]: +# tag_data = tags.get(item) +# parsed_data = parse_iso_date(tag_data) +# if is_iso_string(parsed_data) +# return parsed_data + + + +def get_iso_date_in_file(file_path): + tags = ffprobe_get_tags(file_path) + + tag_TDAT = tags.get("TDAT") + tag_date = tags.get("date") + + parsed_TDAT = parse_TDAT_tag(tag_TDAT) + parsed_date = parse_iso_date(tag_date) + + if parsed_TDAT is None and parsed_date is None: + logger.error(f"no valid date found in '{file_path}' - TDAT: '{tag_TDAT}', date: '{tag_date}'") + raise SystemExit(3) + + else: + logger.debug(f"TDAT: '{parsed_TDAT}' ('{tag_TDAT}'), date: '{parsed_date}' ('{tag_date}')") + logger.debug(f"date: {parsed_date}") + + if parsed_TDAT != parsed_date: + logger.debug(f"dates in 'TDAT' ({parsed_TDAT}) and 'date' ({parsed_date}) differ!") + + if parsed_date is not None: + return parsed_date + else: + return parsed_TDAT + + +# def file_dates_are_ok(file_path): +# tags = ffprobe_get_tags(file_path) +# tag_date = tags.get("date") +# try: +# dt = datetime.fromisoformat(tag_date) +# ts = time.mktime(dt.timetuple()) + +# os.stat(file_path).st_mtime == ts +# except ValueError: +# return False + +def st_time_to_iso(st_time): + return datetime.fromtimestamp(st_time).isoformat() + +def show_info(file_path): + statinfo = os.stat(file_path) + + atime = st_time_to_iso(statinfo.st_atime) + mtime = st_time_to_iso(statinfo.st_mtime) + ctime = st_time_to_iso(statinfo.st_ctime) + logger.info(f"atime: {atime}") + logger.info(f"mtime: {mtime}") + logger.info(f"ctime: {ctime}") + + + +def set_utime(file_path, iso_date_str): + # settings access and modified times + dt = dateutil.parser.parse(iso_date_str) + ts = time.mktime(dt.timetuple()) + # shutil.move(file_path, f"{file_path}.new") + # shutil.move(f"{file_path}.new", file_path) + os.utime(file_path, (ts, ts)) + try: + os.utime(os.path.dirname(file_path), (ts, ts)) + except FileNotFoundError: + pass + return dt + +def eyed3_dates(podcast_file, date): + a = eyed3.load(podcast_file) + + +def mutagen_dates(podcast_file, date): + id3 = ID3(podcast_file) + print(type(id3)) + + + + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("action", choices=["show", "utime-only", "fix-dates", "fix-album-tag"]) + parser.add_argument("--podcast-file") + parser.add_argument("--debug", action="store_true") + parser.add_argument("--quiet", action="store_true") + parser.add_argument("--ffmpeg-out-file", default=f"/tmp/out-{os.getpid()}.mp3") + parser.add_argument("--metadata-util", default="eyed3", choices=["mutagen", "eyeD3", "ffmpeg"], type=str.lower) + parser.add_argument("--utime-only", action="store_true", help="only set utime, no mp3 metadata") + parser.add_argument("--podcast-name") + args = parser.parse_args() + + if not args.debug: + logger.remove() + + if args.quiet: + logger.add(sys.stderr, level="ERROR") + else: + logger.add(sys.stderr, level="INFO") + + return args + + +def main(): + args = parse_args() + logger.debug(f"checking: '{os.path.basename(args.podcast_file)}'") + + date = get_iso_date_in_file(args.podcast_file) + + if args.action == "show": + show_info(args.podcast_file) + + if args.action == "utime-only": + dt = set_utime(args.podcast_file, date) + logger.info(f"set mtime for '{os.path.basename(args.podcast_file)}' to '{dt.isoformat()}' according to mp3 metadata") + + + if args.action == "fix-dates": + # if file_dates_are_ok(args.podcast_file): + # logger.info(f"metadata date and filesystem utimes are ok for {args.podcast_file}', did not modify file") + # else: + if args.metadata_util == "ffmpeg": + ffmpeg_write_date_tag(args.podcast_file, args.out_file, date) + if args.metadata_util == "eyed3": + eyeD3_write_date_tag(args.podcast_file, date) + eyed3_dates(args.podcast_file, date) + if args.metadata_util == "mutagen": + mutagen_dates(args.podcast_file, date) + + + set_utime(args.podcast_file, date) + logger.success(f"updated dates (metadata and file attributes) for '{args.podcast_file}' as {date}") + + if args.action == "fix-album-tag": + podcast_name = get_podcast_name_from_dir(args.podcast_file) + if podcast_name is not None: + eyeD3_write_album_tag(args.podcast_file, podcast_name) + logger.info(f"set album tag to '{podcast_name}' for '{args.podcast_file}'") + + + +if __name__ == "__main__": + main() diff --git a/roles/audiobookshelf/files/pods.py b/roles/audiobookshelf/files/pods.py new file mode 100755 index 0000000..a522c60 --- /dev/null +++ b/roles/audiobookshelf/files/pods.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 + +import urllib.parse +import requests +import json +import sys +import os + +import dateutil.parser +from loguru import logger + +abs = "https://url" +authheaders = { + "Authorization": "Bearer $token"} + +try: + OUTPUT_DIR = sys.argv[1] +except IndexError: + OUTPUT_DIR = "/tmp/abs_metadata" + +logger.info(f"saving metadata json files to '{OUTPUT_DIR}'") + +s = requests.Session() +s.headers.update(authheaders) + +# idea: what is /s/? socket? tracks progress? +# check with browser dev tools +# could add nginx conf/proxy to inject auth header +# or something if i get progress tracking for free +# or custom proxy +# +# rss feed needs to be open for /feed/ urls to +# work. rss feed can be opened with api +# +# find if episode has been played +# https://api.audiobookshelf.org/#items-listened-to +# +# check with browser dev tools what happens when +# the button is clicked to mark an episode as played +# ANSWER: +# curl \ +# 'https://{url}/api/me/progress/li_eadrvpei17yz1unifv/ep_8zz0zme6qtzq9rio8y' \ +# -X PATCH \ +# -H 'Authorization: Bearer $token' \ +# -H 'Accept: application/json, text/plain, */*' \ +# -H 'Accept-Language: en-US,en;q=0.5' \ +# -H 'Accept-Encoding: gzip, deflate, br' \ +# -H 'Content-Type: application/json' \ +# -H 'Referer: https://{url}/item/li_eadrvpei17yz1unifv' \ +# -H 'Origin: https://{url}' \ +# -H 'DNT: 1' \ +# -H 'Sec-Fetch-Dest: empty' \ +# -H 'Sec-Fetch-Mode: cors' \ +# -H 'Sec-Fetch-Site: same-origin' \ +# -H 'Sec-GPC: 1' \ +# -H 'Connection: keep-alive' \ +# -H 'Pragma: no-cache' -H 'Cache-Control: no-cache' \ +# -H 'TE: trailers' \ +# --data-raw '{"isFinished":true}' +# +# $ curl -sX GET 'https://{url}/api/me/progress/li_eadrvpei17yz1unifv/ep_8zz0zme6qtzq9rio8y' -H 'Authorization: Bearer ${token}' | jq . +# { +# "duration": 0, +# "progress": 1, +# "currentTime": 0, +# "isFinished": true, +# "hideFromContinueListening": false, +# "finishedAt": 1679184864387 +# } +# ^ removed less interested fields in the response +# +# listening sessions can be streamed? +# +# use hass to remove from playlist when playback +# is finished? + +def playlist(): + + playlists = s.get(f"{abs}/api/playlists").json()["playlists"] + + for playlist in playlists: + for episode in playlist["items"]: + li_id = episode["libraryItemId"] + ep_id = episode["episodeId"] + file_name = episode["episode"]["audioTrack"]["metadata"]["filename"] + encoded_file_name = urllib.parse.quote_plus(file_name) + file_url = f"{abs}/feed/{li_id}/item/{ep_id}/{encoded_file_name}" + + print(file_url) + +def item_embed_metadata(li_id): + + embed_url = f"{abs}/api/tools/item/{li_id}/embed-metadata" + logger.info(embed_url) + try: + r = s.post(embed_url, data={"forceEmbedChapters": False}) + r.raise_for_status() + return r.json() + except requests.exceptions.HTTPError as e: + logger.error(e) + logger.error(r.text) + return None + + +def metadata_main(): + r = s.get(f"{abs}/api/libraries") + j = r.json() + podcasts = [a for a in j['libraries'] if a['mediaType'] == "podcast"] + logger.info(f"podcast libraries: {len(podcasts)}") + for item in podcasts: + metadata_library(podcasts) + + +def metadata_library(podcasts): + for item in podcasts: + r = s.get(f"{abs}/api/libraries/{item['id']}/items") + j = r.json() + lib_items = j['results'] + metadata_library_item(lib_items) + + +def metadata_library_item(lib_items): + logger.info(f"podcasts: {len(lib_items)}") + + for item in lib_items: + name = item['relPath'] + li_id = item['id'] + + #episodes = item['media']['episodes'] + #for episode in episodes: + # episode_id = episode['id'] + # #lib_item = s.get(f"{abs}/api/items/{episode_id}").json() + # #logger.info(f"{name}: {lib_item}") + # #metadata = item_embed_metadata(episode_id) + + lib_item = s.get(f"{abs}/api/items/{li_id}").json() + + item_name = lib_item['media']['metadata']['title'] + item_id = lib_item['id'] + + media = lib_item['media'] + metadata = lib_item['media']['metadata'] + podcast_path = lib_item['path'] + podcast_rel_path = lib_item['relPath'] + + save_dir = f"{OUTPUT_DIR}/{podcast_rel_path}" + logger.info(f"{name} ({item_id}): {save_dir} ") + os.makedirs(save_dir, exist_ok=True) + + podcast = { + 'podcast_name': item_name, + 'podcast_metadata': metadata, + 'feed_url': metadata['feedUrl'], + 'itunes_id': metadata['itunesId'], + 'path': podcast_path, + 'rel_path': podcast_rel_path, + 'abs_ids': { + 'library_id': lib_item['libraryId'], + 'item_id': item_id, + 'folder_id': lib_item['folderId'], + } + } + + episodes = [] + logger.info(f"latest epsiode: {media['episodes'][0]['title']}") + for ep in media['episodes']: + ep_file = ep['audioFile'] + ep_metadata = ep_file['metadata'] + ep_filename = ep_metadata['filename'] + ep_path = ep_metadata['path'] + ep_rel_path = f"{podcast_rel_path}/{ep_filename}" + + published_date = dateutil.parser.parse(ep['pubDate']).isoformat() + published_date_ts = ep['publishedAt'] + episode = { + 'library_item_id': ep['libraryItemId'], + 'id': ep['id'], + 'path': ep_path, + 'rel_path': ep_rel_path, + 'index': ep.get('index'), + 'title': ep['title'], + 'subtitle': ep.get('subititle'), + 'description': ep.get('description'), + 'published_date': published_date, + 'publised_date_ts': published_date_ts, + 'filename': ep_metadata['filename'], + 'ext': ep_metadata['ext'], + 'mtime_ms': int(ep_metadata['mtimeMs']), + 'ctime_ms': int(ep_metadata['ctimeMs']), + 'birthtime_ms': int(ep_metadata['birthtimeMs']), + 'added_at': int(ep_file['addedAt']), + 'updated_at': int(ep_file['updatedAt']), + 'duration': ep_file['duration'], + } + episodes.append(episode) + with open(f'{save_dir}/{ep_filename}.json', 'w') as f: + f.write(json.dumps(episode, indent=4)) + + + with open(f'{save_dir}/metadata_podcast.json', 'w') as f: + f.write(json.dumps(podcast, indent=4)) + + full_metadata = podcast.copy() + full_metadata['episodes'] = episodes.copy() + + with open(f'{save_dir}/metadata.json', 'w') as f: + f.write(json.dumps(full_metadata, indent=4)) + + + +def metadata_embed(item_id): + r = s.post(f"{abs}/api/tools/item/{tem_id}/embed-metadata") + print(r.text) + print(r.status_code) + +if __name__ == "__main__": + metadata_main() + + diff --git a/roles/audiobookshelf/tasks/audiobookshelf.yml b/roles/audiobookshelf/tasks/audiobookshelf.yml index 0889f83..4180783 100644 --- a/roles/audiobookshelf/tasks/audiobookshelf.yml +++ b/roles/audiobookshelf/tasks/audiobookshelf.yml @@ -84,6 +84,28 @@ - abs-container - docker-containers +- name: install python utilies for mp3 metadata + apt: + name: + - eyed3 + - python3-mutagen + state: present + tags: + - packages + - audiobookshelf-scripts + +- name: config file for podcast tools + copy: + dest: /usr/local/bin/podcasts.json + owner: root + group: "{{ audiobookshelf_group.gid }}" + mode: 0750 + content: "{{ podcast_tools_config | to_nice_json }}" + tags: + - abs-scripts + - audiobookshelf-scripts + - podcast-tools + - name: copy abs scripts copy: src: "{{ item }}" @@ -96,3 +118,19 @@ tags: - abs-scripts - audiobookshelf-scripts + - podcast-tools + +- name: cron file + template: + src: audiobookshelf-cron.j2 + dest: /etc/cron.d/audiobookshelf + owner: root + group: root + mode: 0600 + tags: + - cron + - abs-cron + - audiobookshelf-cron + - abs-scripts + - audiobookshelf-scripts + - podcast-tools diff --git a/roles/audiobookshelf/templates/02-audiobookshelf.conf.j2 b/roles/audiobookshelf/templates/02-audiobookshelf.conf.j2 index 0461508..b8f5db9 100644 --- a/roles/audiobookshelf/templates/02-audiobookshelf.conf.j2 +++ b/roles/audiobookshelf/templates/02-audiobookshelf.conf.j2 @@ -1,3 +1,7 @@ +map $request_uri $kill_stupid_serviceworker_cache { + ~*^/_nuxt/(.*)\.js$ 1; +} + server { listen 443 ssl http2; include listen-proxy-protocol.conf; @@ -22,8 +26,6 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $host; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; proxy_http_version 1.1; @@ -34,8 +36,25 @@ server { add_header "Content-Type" "application/rss+xml"; } + location /socket.io/ { + include /etc/nginx/require_auth.conf; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Protocol $scheme; + proxy_set_header X-Forwarded-Host $http_host; + proxy_pass http://{{ bridgewithdns.audiobookshelf }}:80; + } location / { include /etc/nginx/require_auth.conf; + if ($kill_stupid_serviceworker_cache) { + rewrite "^(.*)$" "$1?id=$request_id" redirect; + } + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $host; @@ -44,6 +63,16 @@ server { proxy_http_version 1.1; + # nuke cache + add_header Last-Modified $date_gmt always; + add_header Cache-Control 'no-store' always; + if_modified_since off; + expires off; + etag off; + + # nuke the service worker cache + sub_filter '.js' '.js?id=$request_id'; + proxy_pass http://{{ bridgewithdns.audiobookshelf }}:80; proxy_redirect http:// https://; } diff --git a/roles/audiobookshelf/templates/audiobookshelf-cron.j2 b/roles/audiobookshelf/templates/audiobookshelf-cron.j2 new file mode 100644 index 0000000..a6116ae --- /dev/null +++ b/roles/audiobookshelf/templates/audiobookshelf-cron.j2 @@ -0,0 +1,6 @@ +# m h dom mon dow command + +*/1 * * * * {{ audiobookshelf_user.username }} find {{ audiobookshelf_path }}/podcasts -mmin -20 -name "*.mp3" -exec /usr/local/bin/fix-podcast-date.py fix-dates --podcast-file {} {# --quiet #}\; + + +# diff --git a/roles/hass/defaults/main.yml b/roles/hass/defaults/main.yml index 551bba9..ed97d53 100644 --- a/roles/hass/defaults/main.yml +++ b/roles/hass/defaults/main.yml @@ -1,2 +1 @@ --- -blink1_enabled: false diff --git a/roles/hass/files/packages/adaptive_lighting.yaml b/roles/hass/files/packages/adaptive_lighting.yaml new file mode 100644 index 0000000..9c69ed4 --- /dev/null +++ b/roles/hass/files/packages/adaptive_lighting.yaml @@ -0,0 +1,141 @@ +--- + +input_number: + sun_adaptive_max_brightness_pct: + name: sun_adaptive_max_brightness_pct + min: 0 + max: 100 + step: 1.0 + mode: box + unit_of_measurement: "%" + + +template: + - sensor: + - name: sun_zenith + unit_of_measurement: "°" + state_class: measurement + icon: >- + {% if states("sun.sun") == "above_horizon" %} + {% if state_attr("sun.sun", "rising") %} + mdi:weather-sunset-up + {% else %} + mdi:white-balance-sunny + {% endif %} + {% else %} + mdi:moon-waning-crescent + {% endif%} + state: >- + {{ state_attr("sun.sun", "elevation") | default(0.0) | float }} + attributes: + friendly_name: Solar zenith angle + below_horizon: >- + {{ states("sun.sun") == "below_horizon" }} + setting: >- + {{ state_attr("sun.sun", "rising") == false }} + + # - name: sun_position + # state_class: measurement + # icon: mdi:white-balance-sunny + # state: >- + # {{ state_attr("switch.adaptive_lighting_adapt_brightness_home", "sun_position") }} + + - name: sun_adaptive_brightness + unit_of_measurement: "%" + state_class: measurement + icon: >- + {{ state_attr("sensor.sun_zenith", "icon") }} + state: >- + {% set sun_zenith_pct = states("sensor.sun_zenith_pct") | float %} + {% set max_brightness_pct = states("input_number.sun_adaptive_max_brightness_pct") | default(100.0) | float %} + {% set brightness_pct = 100.0 - sun_zenith_pct %} + {{ brightness_pct | int }} + attributes: + friendly_name: Adaptive light brightness + + - name: sun_zenith_pct + unit_of_measurement: "%" + state_class: measurement + icon: >- + {{ state_attr("sensor.sun_zenith", "icon") }} + state: >- + {% set sun_highest = states("sensor.sun_zenith_highest") | default(1.0) | float %} + {% set sun_zenith = states("sensor.sun_zenith") | default(0.0) | float %} + {% set daylight_pct = max(sun_zenith, 0) / sun_highest * 100.0 %} + {{ daylight_pct | round(1) }} + + # - name: sun_time_until_dawn + # unit_of_measurement: minutes + # device_class: timestamp + # state_class: measurement + # state: >- + # {% set sun_dawn = state_attr("sun.sun", "next_dawn") | as_datetime %} + # {{ sun_dawn - now() }} + # attributes: + # friendly_name: "Time until dawn" + + # - name: sun_time_until_dusk + # unit_of_measurement: minutes + # device_class: timestamp + # state_class: measurement + # state: >- + # {% set sun_dusk = state_attr("sun.sun", "next_dusk") | as_datetime %} + # {{ sun_dusk - now() }} + # attributes: + # friendly_name: "Time until dusk" + + # - name: sun_time_until_midnight + # state_class: measurement + # device_class: timestamp + # unit_of_measurement: minutes + # state: >- + # {% set sun_midnight = state_attr("sun.sun", "next_midnight") | as_datetime %} + # {{ sun_midnight - now() }} + # attributes: + # friendly_name: "Time until midnight" + + # - name: sun_time_until_noon + # unit_of_measurement: minutes + # state_class: measurement + # device_class: timestamp + # state: >- + # {% set sun_noon = state_attr("sun.sun", "next_noon") | as_datetime %} + # {{ now() - sun_noon - now() }} + # attributes: + # friendly_name: "Time until noon" + + # - name: sun_time_until_rising + # unit_of_measurement: minutes + # device_class: timestamp + # state_class: measurement + # state: >- + # {% set sun_rising = state_attr("sun.sun", "next_rising") | as_datetime %} + # {{ sun_rising - now() }} + # attributes: + # friendly_name: "Time until rising" + + # - name: sun_time_until_setting + # unit_of_measurement: minutes + # device_class: timestamp + # state_class: measurement + # state: >- + # {% set sun_setting = state_attr("sun.sun", "next_setting") | as_datetime %} + # {{ sun_setting - now() }} + # attributes: + # friendly_name: "Time until setting" + + +sensor: + - name: sun_zenith_highest + platform: statistics + entity_id: sensor.sun_zenith + state_characteristic: value_max + max_age: + hours: 24 + + - name: sun_zenith_lowest + platform: statistics + entity_id: sensor.sun_zenith + state_characteristic: value_min + max_age: + hours: 24 diff --git a/roles/hass/files/packages/grow_lights.yaml b/roles/hass/files/packages/grow_lights.yaml index 23a14bc..3b37e7f 100644 --- a/roles/hass/files/packages/grow_lights.yaml +++ b/roles/hass/files/packages/grow_lights.yaml @@ -124,28 +124,6 @@ script: value: "{{ brightness_steps | int }}" automation: - - alias: grow_lights_1_state - trigger: - - platform: time_pattern - minutes: /15 - condition: [] - action: - - service: >- - script.grow_lights_1_{{ states('binary_sensor.grow_lights_1_schedule') }} - data: {} - - - alias: grow_lights_1_hass_start - trigger: - - platform: homeassistant - event: start - condition: [] - action: - - service: >- - light.turn_{{ states('binary_sensor.grow_lights_1_schedule') }} - target: - entity_id: light.grow_lights_1 - data: {} - - alias: grow_lights_1_schedule trigger: - platform: state @@ -161,6 +139,27 @@ automation: target: entity_id: light.grow_lights_1 data: {} + - alias: grow_lights_1_cron_send_state + trigger: + - platform: time_pattern + minutes: /10 + condition: [] + action: + - if: + - condition: state + entity_id: binary_sensor.grow_lights_1_schedule + state: "on" + then: + - service: script.grow_lights_1_on + data: {} + - if: + - condition: state + entity_id: binary_sensor.grow_lights_1_schedule + state: "off" + then: + - service: script.grow_lights_1_off + data: {} + mode: single binary_sensor: diff --git a/roles/hass/files/packages/moosetv.yaml b/roles/hass/files/packages/moosetv.yaml new file mode 100644 index 0000000..140e845 --- /dev/null +++ b/roles/hass/files/packages/moosetv.yaml @@ -0,0 +1,8 @@ +--- + +binary_sensor: + - platform: tod + unique_id: moosetv_schedule + name: moosetv_shedule + after: "06:00" + before: "01:00" diff --git a/roles/hass/handlers/main.yml b/roles/hass/handlers/main.yml index e473eca..45b38f5 100644 --- a/roles/hass/handlers/main.yml +++ b/roles/hass/handlers/main.yml @@ -18,6 +18,7 @@ when: - hass_container is not defined or not hass_container.changed - hass_container_state|default("stopped") == "started" + - hass_restart_handler|default(true) - name: restart zwavejs container docker_container: @@ -30,3 +31,7 @@ - name: udevadm reload rules command: udevadm control --reload-rules + +- name: git-hass-config.sh + command: /usr/local/bin/git-hass-config.sh + become_user: "{{ systemuserlist.hass.username }}" diff --git a/roles/hass/tasks/hass.yml b/roles/hass/tasks/hass.yml index bb0b098..692318f 100644 --- a/roles/hass/tasks/hass.yml +++ b/roles/hass/tasks/hass.yml @@ -69,7 +69,7 @@ - hass-git - hass-git-clone -- name: home assistant config files +- name: home assistant config files and config packages template: src: "{{ item }}.j2" dest: "{{ systemuserlist.hass.home }}/home-assistant/config/{{ item }}" @@ -83,13 +83,14 @@ - templates.yaml - automations-ansible-managed.yaml - scripts-ansible-managed.yaml - - blink1.yaml # - packages/climate.yaml + - packages/blink1.yaml - packages/toothbrush.yaml + - packages/glados_tts.yaml tags: - hass-config -- name: copy config files +- name: copy config packages copy: src: "{{ item }}" dest: "{{ systemuserlist.hass.home }}/home-assistant/config/{{ item }}" @@ -101,8 +102,11 @@ - packages/usb_led_strings.yaml - packages/grow_lights.yaml - packages/fans.yaml + - packages/moosetv.yaml + - packages/adaptive_lighting.yaml tags: - hass-config + - hass-packages - name: copy dashboard config files copy: @@ -126,6 +130,7 @@ mode: 0775 owner: hass group: hass + notify: git-hass-config.sh tags: - hass-git diff --git a/roles/hass/templates/01-hass.conf.j2 b/roles/hass/templates/01-hass.conf.j2 index 8e7c0db..b645357 100644 --- a/roles/hass/templates/01-hass.conf.j2 +++ b/roles/hass/templates/01-hass.conf.j2 @@ -16,6 +16,14 @@ server { server_name {{ hass_url }}; + location /glados/ { + # TEMP: while glados hass integration is WIP + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass {{ hass_glados_tts_url }}; + + } + location / { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; @@ -33,7 +41,7 @@ server { # autoindex_exact_size off; # } - {% if blink1_enabled -%} + {% if blink1_server_port is defined -%} location /blink1/ { {% for cidr in my_local_cidrs -%} allow {{ cidr }}; @@ -139,6 +147,8 @@ server { ssl_session_timeout 5m; ssl_certificate /usr/local/etc/certs/{{ domain }}/fullchain.pem; ssl_certificate_key /usr/local/etc/certs/{{ domain }}/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers off; fastcgi_hide_header X-Powered-By; } diff --git a/roles/hass/templates/01-zwavejs.conf.j2 b/roles/hass/templates/01-zwavejs.conf.j2 index 09b6bce..a3a7e8b 100644 --- a/roles/hass/templates/01-zwavejs.conf.j2 +++ b/roles/hass/templates/01-zwavejs.conf.j2 @@ -1,3 +1,8 @@ +map $http_upgrade $connection_upgrade { + default upgrade; + #default $http_connection; + '' close; +} server { listen 443 ssl http2; {% if inventory_hostname in wg_clients -%} diff --git a/roles/hass/templates/blink1.yaml.j2 b/roles/hass/templates/blink1.yaml.j2 deleted file mode 100644 index 7c27e6b..0000000 --- a/roles/hass/templates/blink1.yaml.j2 +++ /dev/null @@ -1,71 +0,0 @@ -{% if blink1_enabled -%} -friendly_name: blink1 -value_template: >- - {% raw -%} - {{ state_attr('sensor.blink1', 'rgb') != "#000000" }} - {% endraw %} - -# color_template: >- -# {% raw -%} -# {{ state_attr('sensor.blink1', 'rgb') }} -# {% endraw %} - -turn_on: - - service: rest_command.blink1_turn_on - - delay: - milliseconds: 500 - - service: homeassistant.update_entity - target: - entity_id: sensor.blink1 -turn_off: - - service: rest_command.blink1_turn_off - - delay: - milliseconds: 500 - - service: homeassistant.update_entity - target: - entity_id: sensor.blink1 -set_color: - - service: rest_command.blink1_turn_off - - service: rest_command.blink1_set_color - data: - # https://github.com/velijv/home-assistant-color-helpers#rgb-to-hex - # https://community.home-assistant.io/t/advanced-light-template-help/175654 - # https://community.home-assistant.io/t/using-hsv-hsb-to-set-colored-lights/15472 - rgb: >- - {%raw%} - {%- set h2 = h / 360 -%} - {%- set s2 = s / 100 -%} - {%- set v = 100 -%} - {%- set i = (h2 * 6 ) | round(2,'floor') | int-%} - {%- set f = h2 * 6 - i -%} - {%- set p = v * (1 - s2) -%} - {%- set q = v * (1 - f * s2) -%} - {%- set t = v * (1 - (1 - f) * s2) -%} - {%- if i % 6 == 0 -%} - {%- set r = v | int -%} - {%- set g = t | int -%} - {%- set b = p | int -%} - {%- elif i % 6 == 1 -%} - {%- set r = q | int -%} - {%- set g = v | int -%} - {%- set b = p | int -%} - {%- elif i % 6 == 2 -%} - {%- set r = p | int -%} - {%- set g = v | int -%} - {%- set b = t | int -%} - {%- elif i % 6 == 3 -%} - {%- set r = p | int -%} - {%- set g = q | int -%} - {%- set b = v | int -%} - {%- elif i % 6 == 4 -%} - {%- set r = t | int -%} - {%- set g = p | int -%} - {%- set b = v | int -%} - {%- elif i % 6 == 5 -%} - {%- set r = v | int -%} - {%- set g = p | int -%} - {%- set b = q | int -%} - {%- endif -%} - {{ '%02x%02x%02x' | format(r, g, b) }} - {%endraw%} -{% endif %} diff --git a/roles/hass/templates/configuration.yaml.j2 b/roles/hass/templates/configuration.yaml.j2 index 15006e4..ab69217 100644 --- a/roles/hass/templates/configuration.yaml.j2 +++ b/roles/hass/templates/configuration.yaml.j2 @@ -64,6 +64,17 @@ scene: !include scenes.yaml template: !include templates.yaml #climate: !include climate.yaml +http: + server_host: 127.0.0.1 + server_port: 8123 + trusted_proxies: + - 127.0.0.1 + use_x_forwarded_for: true + +api: + +websocket_api: + # Text to speech tts: - platform: voicerss @@ -82,15 +93,6 @@ calendar: url: {{ item.url | trim }} {% endfor %} -http: - server_host: 127.0.0.1 - server_port: 8123 - trusted_proxies: - - 127.0.0.1 - use_x_forwarded_for: true - -api: - frontend: themes: !include_dir_merge_named themes @@ -261,16 +263,6 @@ sensor: #{% endfor %} #} - {% if blink1_enabled -%} - - platform: rest - resource: http://localhost:{{ blink1_server_port }}/blink1 - name: blink1 - json_attributes: - - rgb - - bright - value_template: "{%raw%}{{ value_json.rgb }}{%endraw%}" - {% endif %} - binary_sensor: - platform: workday country: DE @@ -335,13 +327,13 @@ device_tracker: # to known_devices.yaml when they are discovered, but they wont be # tracked unless 'track' is set to 'true' for a device there (edit # the file to track a discovered device). - track_new_devices: true + track_new_devices: false #new_device_defaults: # track_new_devices: false - platform: bluetooth_tracker request_rssi: true - track_new_devices: true + track_new_devices: false - platform: ping # these are probably not used, because they are "global settings" # and only the first values from the first platform (bluetooth_le_tracker) @@ -405,32 +397,6 @@ notify: shell_command: matrixmsg: /usr/local/bin/matrixmsg.py -rest_command: - {% if blink1_enabled -%} - blink1_turn_on: - url: {{ hass_blink1_url }}/blink1/on?bright=250 - #url: http://localhost:{{ blink1_server_port }}/blink1/fadeToRGB?rgb=ff0ff - method: GET - content_type: "application/json" - blink1_turn_off: - url: {{ hass_blink1_url }}/blink1/off - method: GET - content_type: "application/json" - blink1_turn_magenta: - url: {{ hass_blink1_url }}/blink1/fadeToRGB?rgb=ff00ff - method: GET - content_type: "application/json" - blink1_set_color: - url: "{{ hass_blink1_url }}/blink1/fadeToRGB?rgb={%raw%}{{ rgb }}{%endraw%}" - method: GET - {% endif %} - -light: - {% if blink1_enabled -%} - - platform: template - lights: - blink1: !include blink1.yaml - {% endif %} {# # feedreader: diff --git a/roles/hass/templates/git-hass-config.sh.j2 b/roles/hass/templates/git-hass-config.sh.j2 index 31c8215..4e38396 100644 --- a/roles/hass/templates/git-hass-config.sh.j2 +++ b/roles/hass/templates/git-hass-config.sh.j2 @@ -25,32 +25,34 @@ fi mkdir -p ${PATH_REPO}/config/ mkdir -p ${PATH_REPO}/config/.storage -{% for item in hass_config_repo_cp -%} -{% if item.dir|default(false) %}{% set cp_r = "r" -%} -{% else %}{% set cp_r = "" -%} -{% endif -%} -{% if item.src.endswith("*") -%} -{% if item.src.count("/") > 0 -%} -{% set dest = item.src.split("/")[:-1] | join("/") + "/" -%} -{% else -%} -{% set dest = "" -%} -{% endif -%} -{% else -%} -{% set dest = item.src %} -{% endif %} -cp -a{{ cp_r }} ${PATH_HASS}/config/{{ item.src }} ${PATH_REPO}/config/{{ dest }} +{% for item in hass_config_repo_cp_files -%} +cp -a ${PATH_HASS}/config/{{ item }} ${PATH_REPO}/config/{{ item }} {% endfor %} +{% for item in hass_config_repo_cp_dirs -%} +cp -ra ${PATH_HASS}/config/{{ item }} ${PATH_REPO}/config/ +{% endfor %} +{% for item in hass_config_repo_cp_globs -%} +{% set dest_dir = item.split('/')[:-1] | join("/") %} +cp -ra ${PATH_HASS}/config/{{ item }} ${PATH_REPO}/config/{{ dest_dir }} +{% endfor %} + +set +e + +{% for item in hass_config_repo_rm -%} +git rm -rf ${PATH_REPO}/config/{{ item }} &> /dev/null +{% endfor %} + +set -e if test -n "$(git status --porcelain)" ; then - {% for item in hass_config_repo_cp -%} - git add config/{{ item.src }} > /dev/null - {% endfor %} - + git add config/ > /dev/null git commit -m "config updated" > /dev/null fi git pull --quiet -git push --quiet 2> /dev/null +git push origin main --quiet 2> /dev/null +#git push test main --force + # TODO: copy changes from git diff --git a/roles/hass/templates/packages/blink1.yaml.j2 b/roles/hass/templates/packages/blink1.yaml.j2 new file mode 100644 index 0000000..d0decf7 --- /dev/null +++ b/roles/hass/templates/packages/blink1.yaml.j2 @@ -0,0 +1,111 @@ +--- + +template: + - binary_sensor: + - name: "blink1_on" + device_class: light + state: >- + {% raw -%} + {{ state_attr('sensor.blink1', 'rgb') != "#000000" }} + {% endraw %} + +sensor: + - platform: rest + resource: http://localhost:{{ blink1_server_port }}/blink1 + name: blink1 + json_attributes: + - rgb + - bright + value_template: "{%raw%}{{ value_json.rgb }}{%endraw%}" + +rest_command: + blink1_turn_on: + url: {{ hass_blink1_url }}/blink1/on?bright=250 + #url: http://localhost:{{ blink1_server_port }}/blink1/fadeToRGB?rgb=ff0ff + method: GET + content_type: "application/json" + blink1_turn_off: + url: {{ hass_blink1_url }}/blink1/off + method: GET + content_type: "application/json" + blink1_turn_magenta: + url: {{ hass_blink1_url }}/blink1/fadeToRGB?rgb=ff00ff + method: GET + content_type: "application/json" + blink1_set_color: + url: "{{ hass_blink1_url }}/blink1/fadeToRGB?rgb={%raw%}{{ rgb }}{%endraw%}" + method: GET + +light: + - platform: template + lights: + blink1: + friendly_name: blink1 + value_template: >- + {% raw -%} + {{ state_attr('sensor.blink1', 'rgb') != "#000000" }} + {% endraw %} + + # color_template: >- + # {% raw -%} + # {{ state_attr('sensor.blink1', 'rgb') }} + # {% endraw %} + + turn_on: + - service: rest_command.blink1_turn_on + - delay: + milliseconds: 500 + - service: homeassistant.update_entity + target: + entity_id: sensor.blink1 + turn_off: + - service: rest_command.blink1_turn_off + - delay: + milliseconds: 500 + - service: homeassistant.update_entity + target: + entity_id: sensor.blink1 + set_color: + - service: rest_command.blink1_turn_off + - service: rest_command.blink1_set_color + data: + # https://github.com/velijv/home-assistant-color-helpers#rgb-to-hex + # https://community.home-assistant.io/t/advanced-light-template-help/175654 + # https://community.home-assistant.io/t/using-hsv-hsb-to-set-colored-lights/15472 + rgb: >- + {%raw%} + {%- set h2 = h / 360 -%} + {%- set s2 = s / 100 -%} + {%- set v = 100 -%} + {%- set i = (h2 * 6 ) | round(2,'floor') | int-%} + {%- set f = h2 * 6 - i -%} + {%- set p = v * (1 - s2) -%} + {%- set q = v * (1 - f * s2) -%} + {%- set t = v * (1 - (1 - f) * s2) -%} + {%- if i % 6 == 0 -%} + {%- set r = v | int -%} + {%- set g = t | int -%} + {%- set b = p | int -%} + {%- elif i % 6 == 1 -%} + {%- set r = q | int -%} + {%- set g = v | int -%} + {%- set b = p | int -%} + {%- elif i % 6 == 2 -%} + {%- set r = p | int -%}n + {%- set g = v | int -%} + {%- set b = t | int -%} + {%- elif i % 6 == 3 -%} + {%- set r = p | int -%} + {%- set g = q | int -%} + {%- set b = v | int -%} + {%- elif i % 6 == 4 -%} + {%- set r = t | int -%} + {%- set g = p | int -%} + {%- set b = v | int -%} + {%- elif i % 6 == 5 -%} + {%- set r = v | int -%} + {%- set g = p | int -%} + {%- set b = q | int -%} + {%- endif -%} + {{ '%02x%02x%02x' | format(r, g, b) }} + {%endraw%} diff --git a/roles/hass/templates/packages/glados_tts.yaml.j2 b/roles/hass/templates/packages/glados_tts.yaml.j2 new file mode 100644 index 0000000..0a17cb9 --- /dev/null +++ b/roles/hass/templates/packages/glados_tts.yaml.j2 @@ -0,0 +1,23 @@ +--- + +# example of simple tts integration: +# https://github.com/nagyrobi/home-assistant-custom-components-marytts/blob/main/custom_components/marytts/tts.py + +template: + - binary_sensor: + - name: glados_tts_is_in_hass + state: false + +notify: + - name: glados + platform: rest + resource: https://{{ hass_url }}/glados/say.mp3 + method: POST_JSON + headers: + #Authorization: + content-type: application/json + data_template: + message: >- + {% raw -%} + {{ title ~ '\n' ~ message }} + {% endraw %} diff --git a/roles/hass/templates/templates.yaml.j2 b/roles/hass/templates/templates.yaml.j2 index e41a0a3..6afdaa3 100644 --- a/roles/hass/templates/templates.yaml.j2 +++ b/roles/hass/templates/templates.yaml.j2 @@ -66,15 +66,6 @@ #} - binary_sensor: - {% if blink1_enabled -%} - - name: "blink1_on" - device_class: light - state: >- - {% raw -%} - {{ state_attr('sensor.blink1', 'rgb') != "#000000" }} - {% endraw %} - {% endif %} - - name: "heating_on" icon: "mdi:home-thermometer" device_class: heat diff --git a/roles/homeaudio/tasks/homeaudio.yml b/roles/homeaudio/tasks/homeaudio.yml index b85f807..30a4d65 100644 --- a/roles/homeaudio/tasks/homeaudio.yml +++ b/roles/homeaudio/tasks/homeaudio.yml @@ -4,11 +4,20 @@ apt: name: - avahi-utils + - eyed3 - id3v2 - ffmpeg + - pulseaudio-dlna + - pulseaudio-module-bluetooth + #- pulseaudio-module-raop + #- pulseaudio-module-zeroconf + #- pulseaudio-module-lirc + #- pulseaudio-module-jack + state: present tags: - homeaudio-packages + - packages - name: install yt-dlp pip: diff --git a/roles/jellyfin/templates/01-jellyfin.j2 b/roles/jellyfin/templates/01-jellyfin.j2 index c06a736..3fa4046 100644 --- a/roles/jellyfin/templates/01-jellyfin.j2 +++ b/roles/jellyfin/templates/01-jellyfin.j2 @@ -208,7 +208,7 @@ server { # External Javascript (such as cast_sender.js for Chromecast) must # be allowlisted. # 'self' https://*.{{ domain }} https://{{ domain }} - add_header Content-Security-Policy "default-src https: data: blob: http://image.tmdb.org; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.gstatic.com/cv/js/sender/v1/cast_sender.js https://www.gstatic.com/eureka/clank/cast_sender.js https://www.gstatic.com/eureka/clank/94/cast_sender.js https://www.gstatic.com/eureka/clank/95/cast_sender.js https://www.gstatic.com/eureka/clank/96/cast_sender.js https://www.gstatic.com/eureka/clank/97/cast_sender.js https://www.gstatic.com/eureka/clank/105/cast_sender.js https://www.gstatic.com/eureka/clank/106/cast_sender.js https://www.youtube.com blob:; worker-src 'self' blob:; connect-src 'self'; object-src 'none'; frame-ancestors 'self'" always; + add_header Content-Security-Policy "default-src https: data: blob: http://image.tmdb.org; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.gstatic.com/cv/js/sender/v1/cast_sender.js https://www.gstatic.com/eureka/clank/cast_sender.js https://www.gstatic.com/eureka/clank/94/cast_sender.js https://www.gstatic.com/eureka/clank/95/cast_sender.js https://www.gstatic.com/eureka/clank/96/cast_sender.js https://www.gstatic.com/eureka/clank/97/cast_sender.js https://www.gstatic.com/eureka/clank/98/cast_sender.js https://www.gstatic.com/eureka/clank/105/cast_sender.js https://www.gstatic.com/eureka/clank/106/cast_sender.js https://www.gstatic.com/eureka/clank/111/cast_sender.js https://www.youtube.com blob:; worker-src 'self' blob:; connect-src 'self'; object-src 'none'; frame-ancestors 'self'" always; # kill cache #add_header date $date_gmt always; diff --git a/roles/owntone/templates/owntone-cron.j2 b/roles/owntone/templates/owntone-cron.j2 index c6c3415..2b3f9d6 100644 --- a/roles/owntone/templates/owntone-cron.j2 +++ b/roles/owntone/templates/owntone-cron.j2 @@ -3,5 +3,6 @@ # m h dom mon dow */60 * * * * {{ owntone_user.username }} touch {{ owntone_path }}/audio/trigger.init-rescan +08 08 * * * {{ owntone_user.username }} touch {{ owntone_path }}/audio/trigger.meta-rescan # diff --git a/roles/owntone/templates/owntone.conf.j2 b/roles/owntone/templates/owntone.conf.j2 index 3f6643c..98fcb4e 100644 --- a/roles/owntone/templates/owntone.conf.j2 +++ b/roles/owntone/templates/owntone.conf.j2 @@ -4,7 +4,7 @@ general { # below, and full access to the databases, log and local audio uid = "owntone" - db_path = "/config/dbase_and_logs/songs3.db" + db_path = "/config/{{ owntone_db }}" # Database backup location # Uncomment and specify a full path to enable abilty to use REST endpoint @@ -32,8 +32,14 @@ general { # client types like Remotes, DAAP clients (iTunes) and to the web # interface. Options are "any", "localhost" or the prefix to one or # more ipv4/6 networks. The default is { "localhost", "192.168", "fd" } - {% set s21 = s21_cidr.split(".")[:3] | join(".") -%} - trusted_networks = { "localhost", "{{ s21 }}", "fd" } + + trusted_networks = { + {% for item in owntone_networks -%} + "{{ item.split(".")[:3] | join(".") | trim }}", + {% endfor -%} + "localhost", + "fd" + } # Enable/disable IPv6 ipv6 = no @@ -112,7 +118,7 @@ library { # albums and artists with only one track. The tracks will still be # visible in other lists, e.g. songs and playlists. This setting # currently only works in some remotes. - #hide_singles = false + hide_singles = true # Internet streams in your playlists will by default be shown in the # "Radio" library, like iTunes does. However, some clients (like diff --git a/roles/wireguard/templates/wg0.conf.j2 b/roles/wireguard/templates/wg0.conf.j2 index 49122bc..a35daba 100644 --- a/roles/wireguard/templates/wg0.conf.j2 +++ b/roles/wireguard/templates/wg0.conf.j2 @@ -3,7 +3,7 @@ ## NEW [Interface] -Address = {{ wg.ip }}/32 +Address = {{ wg.ip }}/24 {% if wg.listen|default(false) %} ListenPort = {{ wireguard_port }} {% endif %} -- 2.40.1