recent changes #59
|
@ -10,6 +10,7 @@ airconnect_group:
|
|||
|
||||
# for flac: 'flc'
|
||||
airconnect_codec: mp3:320
|
||||
airconnect_latency: 1000:2000
|
||||
|
||||
airconnect_max_volume: "100"
|
||||
airconnect_upnp: []
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<metadata>1</metadata>
|
||||
<flush>1</flush>
|
||||
<artwork></artwork>
|
||||
<latency>0:500</latency>
|
||||
<latency>{{ airconnect_latency }}</latency>
|
||||
<drift>0</drift>
|
||||
</common>
|
||||
<main_log>info</main_log>
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
|
@ -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()
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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://;
|
||||
}
|
||||
|
|
|
@ -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 #}\;
|
||||
|
||||
|
||||
#
|
|
@ -1,2 +1 @@
|
|||
---
|
||||
blink1_enabled: false
|
||||
|
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
|
||||
binary_sensor:
|
||||
- platform: tod
|
||||
unique_id: moosetv_schedule
|
||||
name: moosetv_shedule
|
||||
after: "06:00"
|
||||
before: "01:00"
|
|
@ -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 }}"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 -%}
|
||||
|
|
|
@ -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 %}
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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%}
|
|
@ -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 %}
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
||||
#
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
## NEW
|
||||
|
||||
[Interface]
|
||||
Address = {{ wg.ip }}/32
|
||||
Address = {{ wg.ip }}/24
|
||||
{% if wg.listen|default(false) %}
|
||||
ListenPort = {{ wireguard_port }}
|
||||
{% endif %}
|
||||
|
|
Loading…
Reference in New Issue