Compare commits

...
36 Commits
Author SHA1 Message Date
lucas 3bf1fc1be6 add exportfs config
ads
2026-10-14 07:31:43 +02:00
lucas 7b0b438740 add nextcloud app update to upgrade script 2026-10-13 20:15:03 +02:00
lucas 2eda7cd33e align convert heic with mediahost and new picture sync
asd
2026-10-13 20:15:03 +02:00
lucas efc150d9ce ssh config and hosts
asd
2026-10-13 20:15:03 +02:00
lucas 044fbd81ad working smartmon node exporter script for sd/nvme 2026-10-12 21:54:40 +02:00
lucas 01ab319eea node exporter: add hostname dependent docker count and default value for smartd script 2026-10-12 19:19:15 +00:00
lucas d5e627325d add quiethost to smartd 2026-10-12 20:49:53 +02:00
lucas deaff35bc3 remove raspberry from bash_aliases
add mediahost to wol.sh
2026-10-12 20:40:34 +02:00
lucas 27d570fc5c node exporter script for smartd 2026-10-12 18:39:53 +00:00
lucas d5fb01adb2 docker backup serverhost
add cadvisor to serverhost
change node-exporter to network host to see propper netw traffic
refine torrentscomplete script
2026-10-12 20:10:54 +02:00
lucas 52c02beb3d mediahost docker backup
add gite to shared proxy
add usb dev and NET privilige to HA
add immich image server
remove sonarr-en
add jackett indexer for ABB
add gluetun-proxy for general proxy
add blackbox exporter to check https sites
set node-exporter to network host to see interface traffic
decrease resolution of cadvisor to save RAM
add status and code SANs for nginx
add upstream block so sites are not redirecte to cloud...
add homeassistant config
add nginx exporter for prometheus
2026-10-12 18:10:41 +00:00
lucas 3f14f53184 add serverhost to smartd.conf 2026-10-12 17:59:00 +00:00
lucas c41bf36afb add smartd conf 2026-10-12 17:50:54 +00:00
lucas 07bab58ae0 add smartctl service 2026-10-12 15:01:20 +00:00
Lucas Vivo d06cff4cbd compare playlists m3u und navidrome 2026-10-12 16:33:53 +02:00
Lucas Vivo 09343a7523 first version of general audiobook library.py 2026-10-12 16:33:53 +02:00
Lucas Vivo dc0cd02e58 combine audiobooks: remove unecessary function 2026-10-12 16:33:53 +02:00
lucas 7158dd4960 change mqtt broker user to new mediahost impl 2026-10-12 12:07:28 +02:00
lucas 4c299c5a38 move sudo for upgrades into script and call script as user 2026-10-12 10:06:37 +00:00
Lucas Vivo 2f3a763054 adapt nextloud scan to docker 2026-10-09 17:36:49 +02:00
Lucas Vivo f7ff789d81 translate edit genre metadata to python
ads

asd
2026-10-08 16:25:50 +02:00
Lucas Vivo 7c6a121504 finalize metadata edit_database
asd
2026-10-08 16:25:50 +02:00
lucas 144d974731 add log to audiobooks edit database
asd
2026-10-08 14:06:41 +02:00
lucas 9e4901699d move metadate 2026-10-08 12:06:09 +00:00
lucas 8c29731ddc update sed in remove_genre 2026-10-08 12:05:53 +00:00
lucas 80ad918686 add log to audiobooks edit database 2026-10-08 10:38:54 +00:00
Lucas Vivo 3d585d14e0 audiobook edit sqlite database to remove audiobook genre 2026-10-08 12:26:35 +02:00
Lucas Vivo 8b6fc7fdcd combine audiobooks: extend to opus
asd

asd
2026-10-08 12:26:08 +02:00
Lucas Vivo e4f1cce494 combine audiobook better logging 2026-10-07 18:59:18 +02:00
Lucas Vivo 8cccf674cf parallelize creat media function call 2026-10-07 18:36:39 +02:00
Lucas Vivo 86679e4e18 combine audiobooks: move temp dir 2026-10-07 14:58:16 +02:00
Lucas Vivo fc9a2dcf0e combine audiobooks: create audiobook in temp dir
add coloured skip message
2026-10-07 14:16:41 +02:00
Lucas Vivo 57520eaa04 update log print colours 2026-10-07 12:01:20 +02:00
Lucas Vivo f9ee0a0694 add percentage counter 2026-10-07 09:08:14 +02:00
Lucas Vivo 1af47022e9 move combine audiobooks script
chmod
uncomment delete lines
2026-10-07 06:58:25 +02:00
Lucas Vivo ce656b6970 add metadata copy to combine media script 2026-10-07 06:38:36 +02:00
40 changed files with 1623 additions and 421 deletions

No files matched your search

@@ -1,6 +1,7 @@
#!/usr/bin/env python3
from __future__ import annotations
from concurrent.futures import ProcessPoolExecutor, as_completed
import argparse
import subprocess
@@ -8,6 +9,11 @@ import tempfile
from pathlib import Path
import time
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
RESET = "\033[0m"
logfile=Path(f"media_{time.strftime("%Y%m%d_%H%M%S")}.log")
def log_message(message):
@@ -36,25 +42,26 @@ def ffmpeg_escape(path: Path) -> str:
return str(path.resolve()).replace("'", r"'\''")
def create_m4b(root_path: Path, book_dir: Path) -> None:
media_files = sorted(book_dir.glob("*.m[p,4][3,b,a]"))
media_files = sorted(list(book_dir.glob("*.mp3"))
+ list(book_dir.glob("*.m4a"))
+ list(book_dir.glob("*.m4b"))
+ list(book_dir.glob("*.opus")))
if not media_files:
return
output_file_tmp = root_path / "tmp" / f"{book_dir.name}.m4b"
if not output_file_tmp.parent.exists():
output_file_tmp.parent.mkdir()
output_file_final = book_dir / f"{book_dir.name}.m4b"
if output_file_final.exists():
log_message(f"[SKIP] {output_file_final}")
log_message(f"{RED}[SKIP]{RESET} {output_file_final}")
log_message("Book already present")
return
log_message(f"[CREATE] {output_file_final}")
with (tempfile.TemporaryDirectory() as tmp):
with (tempfile.TemporaryDirectory(dir=root_path.parent) as tmp):
tmpdir = Path(tmp)
output_file_tmp = tmpdir / f"{book_dir.name}.m4b"
concat_file = tmpdir / "concat.txt"
metadata_file = tmpdir / "chapters.txt"
@@ -84,9 +91,12 @@ def create_m4b(root_path: Path, book_dir: Path) -> None:
"-map","0:a",
# cover image from first media
"-map","2:v?",
"-map_metadata","1",
# copy artist/album/composer/etc from first media file
"-map_metadata", "2",
# copy chapters from ffmetadata
"-map_chapters", "1",
"-c:a","copy",
"-c:a","copy",
"-c:a", "aac",
"-b:a", "128k",
# copy cover image without re-encoding
@@ -101,8 +111,11 @@ def create_m4b(root_path: Path, book_dir: Path) -> None:
"-map", "0:a",
# cover image from first media
"-map", "2:v?",
"-map_metadata", "1",
# copy artist/album/composer/etc from first media file
"-map_metadata", "2",
# copy chapters from ffmetadata
"-map_chapters", "1",
"-c:a", "copy",
# copy cover image without re-encoding
"-c:v", "copy", "-disposition:v:0", "attached_pic",
@@ -116,7 +129,7 @@ def create_m4b(root_path: Path, book_dir: Path) -> None:
text=True,
)
if result.returncode == 234:
log_message(f"[ERROR] {result.returncode}")
log_message(f"{YELLOW}[WARNING]{RESET} - {book_dir.name} - {result.returncode} retrying with aac encoder")
result = subprocess.run(
cmd_backup,
stdout=log,
@@ -124,35 +137,20 @@ def create_m4b(root_path: Path, book_dir: Path) -> None:
text=True,
)
if result.returncode == 0:
log_message("Success, delete media files")
output_file_tmp.rename(output_file_final)
#for media in media_files:
# media.unlink()
for media in media_files:
media.unlink()
log_message(f"{GREEN}[SUCCESS]{RESET} - {book_dir.name} - delete media files")
else:
log_message(f"FAILED: Code: {result.returncode}, Stderr: {result.stderr}")
log_message(f"{RED}[ERROR]{RESET} - {book_dir.name} - Code: {result.returncode}, Stderr: {result.stderr}")
def find_book_dirs(root: Path) -> list[Path]:
"""
A book directory is any directory containing more than one
audiobook file (.mp3, .m4a, .m4b)
"""
media_files = (
list(root.rglob("*.mp3"))
+ list(root.rglob("*.m4a"))
+ list(root.rglob("*.m4b"))
)
counts: dict[Path, int] = {}
for file in media_files:
counts[file.parent] = counts.get(file.parent, 0) + 1
return sorted(
directory
for directory, count in counts.items()
if count > 1
)
def process_book(args):
target, book_dir = args
try:
create_m4b(target, book_dir)
return book_dir, None
except Exception as exc:
return book_dir, str(exc)
def main() -> None:
parser = argparse.ArgumentParser()
@@ -170,20 +168,32 @@ def main() -> None:
raise SystemExit(f"Path does not exist: {target}")
# Direct book folder
if any(target.glob("*.m[p,4][3,b]")):
if any(list(target.glob("*.mp3"))
+ list(target.glob("*.m4a"))
+ list(target.glob("*.m4b"))
+ list(target.glob("*.opus"))):
create_m4b(target.parent,target)
return
book_dir_set = find_book_dirs(target)
book_dir_set, library_size = find_book_dirs(target)
for book_dir in book_dir_set:
log_message(f"Found Media: {book_dir}")
print("Proceed? y/n")
name = input()
if name != "y":
log_message(f"Aborted")
exit(0)
# Root folder containing many books
for book_dir in book_dir_set:
try:
create_m4b(target,book_dir)
except Exception as exc:
log_message(f"[ERROR] {book_dir}: {exc}")
with ProcessPoolExecutor(max_workers=8) as executor:
for book_dir, error in executor.map(
process_book,
((target, book_dir) for book_dir in book_dir_set),
):
if error:
log_message(f"{RED}[ERROR]{RESET} {book_dir}: {error}")
if __name__ == "__main__":
-60
View File
@@ -1,60 +0,0 @@
#!/usr/bin/env python3
import sys
import subprocess
from pathlib import Path
ROOT = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(".")
audio_extensions = {
".m4b",
".m4a",
".mp3",
".flac",
".ogg",
}
def get_metadata(field, file):
try:
result = subprocess.run(
["kid3-cli", "-c", f"get {field}", str(file)],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
)
return result.stdout.strip()
except (OSError, subprocess.SubprocessError):
return ""
# Include ROOT itself, matching find "$ROOT" -type d
for directory in [ROOT] + [p for p in ROOT.rglob("*") if p.is_dir()]:
audio_files = sorted(
(
file
for file in directory.iterdir()
if file.is_file()
and file.suffix.lower() in audio_extensions
),
key=lambda p: str(p),
)
if not audio_files:
continue
first_file = audio_files[0]
artist = get_metadata("artist", first_file)
album = get_metadata("album", first_file)
composer = get_metadata("composer", first_file)
comment = get_metadata("comment", first_file)
print("====≠===============================")
print(f"Folder: {directory.name}")
print(f"Album: {album}")
print(f"Artist: {artist}")
print(f"Narrator: {composer}")
print(f"Comment: {comment}")
+440
View File
@@ -0,0 +1,440 @@
#!/usr/bin/env python3
import subprocess
import tempfile
import argparse
import logging
import re
from pathlib import Path
from typing import Any
from collections import defaultdict
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
RESET = "\033[0m"
logger = logging.getLogger(__name__)
audio_extensions = {
"*.m4b",
"*.m4a",
"*.mp3",
"*.flac",
"*.ogg",
"*.opus"
}
class Audiobook():
def __init__(self, bookPath: Path) -> None:
self.bookPath = bookPath
self.files = [file for pattern in audio_extensions for file in
bookPath.glob(pattern)]
self.firstFile = self.get_first_file()
self.size = sum(file.stat().st_size for pattern in audio_extensions for file in
bookPath.glob(pattern))
self.fileConunt = len(self.files)
self.singleFile = self.fileConunt > 1
# Metadata
self.loadMetadata()
logger.info(f"Found {self.bookPath.name}, {self.fileConunt} files")
def __repr__(self):
return (
f"Audiobook("
f"name={self.name!r}, "
f"artist={self.artist!r}, "
f"composer={self.composer!r}, "
f"path={str(self.bookPath)!r}),"
f"size={self.size!r}, "
f"fileConunt={self.fileConunt!r}, "
)
def get_first_file(self):
return self.files[0]
def loadMetadata(self):
fields = ["artist", "album", "composer", "comment", "genre"]
metadata = self.get_metadata(self.get_first_file(), fields )
self.artist = metadata["artist"]
self.name = metadata["album"]
self.composer = metadata["composer"]
self.comment = metadata["comment"]
self.genre = metadata["genre"]
def get_metadata(self, file, fields):
metadata=dict.fromkeys(fields, "")
for field in fields:
cmd= (["kid3-cli", "-c", f"get {field}", str(file)])
try:
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
check=True,
)
metadata[field] = result.stdout.strip()
except (OSError, subprocess.SubprocessError) as error:
print(f"Error getting metadata for {file}: {error}")
return metadata
return metadata
def write_metadata(self, file, fields, values):
metadata=dict.fromkeys(fields, "")
for field,value in fields,values:
cmd= (["kid3-cli", "-c" , "tag 2","-c", f"set {field} \"{value}\"", "-c" "save", str(file)])
try:
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
check=True,
)
except (OSError, subprocess.SubprocessError) as error:
print(f"Error writing metadata for {file}: {error}")
return
return
def fixComposer(self):
if not self.composer:
m = re.search(
r"(?:read\s+by|spoken\s+by|narrated\s+by|gesprochen\s+von|gelesen\s+von)\s+([^\n.,;]+)",
self.comment,
re.IGNORECASE,
)
if not m:
logger.info(f"No composer found for {self.bookPath}")
return
logger.info(f"[Updated] composer found for {self.bookPath}")
self.composer = m.group(1).strip()
self.comment = (self.comment[:m.start()] + self.comment[m.end():] ).strip(" .,;\n")
self.comment = re.sub(r"\s+"," ",self.comment)
def fixName(self):
if not self.name:
self.name = self.bookPath.name
logger.info(f"[Updated] name found for {self.bookPath}")
def clean_genre(self,genre):
if not self.genre:
return
genre = self.genre.lower()
genre = re.sub(r'(?i)^audio\s*books?$', '', genre)
genre = re.sub(r'(?i)^audiobooks?;?$', '', genre)
genre = re.sub(r'(?i)\b(hörb(uch|ücher)|hoerb(uch|uecher))\b', '', genre)
genre = re.sub(r'(?i)^(hörspiele?|hoerspiele?)$', '', genre)
genre = re.sub(r'(?i)^spoken\s*word$', '', genre)
genre = re.sub(r'(?i)gesprochene\s*inhalte$', '', genre)
genre = re.sub(r'(?i)^speech$', '', genre)
genre = re.sub(r'Fanasie', 'Fantasie', genre)
genre = re.sub(r'Mit Videospiel-?', 'Videospielbezug', genre)
genre = re.sub(r'(?i)nonfiction', 'NonFiction', genre)
genre = re.sub(r'(?i)^speech$', '', genre)
genre = re.sub(r'(?i)^vocal$', '', genre)
genre = re.sub(r'(?i)^retail$', '', genre)
genre = re.sub(r'(?i)^other$', '', genre)
genre = re.sub(r'(?i)^misc$', '', genre)
genre = re.sub(r'(?i)^(sci[- ]?fi|sf)$', 'Science Fiction', genre)
genre = re.sub(r'(?i)^\(?science fiction\)?$', 'Science Fiction', genre)
genre = re.sub(r'Thrillers', 'Thriller', genre)
genre = re.sub(r"(?i)\bchildren'?s audiobooks?\b", "", genre)
genre = re.sub(r'^-\s*|\s*-$', '', genre)
genre = re.sub(r'(?i)Engelske\s*\sLydbånd', '', genre)
genre = re.sub(r'^FIC028000.*$', '', genre)
genre = re.sub(r'^800.*$', '', genre)
genre = re.sub(r'^Fantastic Beasts.*$', '', genre)
genre = re.sub(r'^and thei.*$', '', genre)
genre = re.sub(r'^those you.*$', '', genre)
# Normalize separators
genre = re.sub(r'\s*[,&:/]\s*',';',genre)
genre = re.sub(r'\s*;\s*','; ',genre)
# Trim punctuation / whitespace
genre = re.sub(r'^[\s,;:\-]+','',genre)
genre = re.sub(r'[\s,;\-]+$','',genre)
genre = genre.strip()
self.genre = genre.lstrip()
return
def create_m4b(self, root_path) -> None:
output_file_final = self.bookPath / f"{self.bookPath.name}.m4b"
if output_file_final.exists():
logger.info(f"{RED}[SKIP]{RESET} {output_file_final}")
logger.info("Book already present")
return
logger.info(f"[CREATE] {output_file_final}")
with (tempfile.TemporaryDirectory(dir=root_path.parent) as tmp):
tmpdir = Path(tmp)
output_file_tmp = tmpdir / f"{self.bookPath.name}.m4b"
concat_file = tmpdir / "concat.txt"
metadata_file = tmpdir / "chapters.txt"
current_ms = 0
with concat_file.open("w", encoding="utf-8") as cf, \
metadata_file.open("w", encoding="utf-8") as mf:
mf.write(";FFMETADATA1\n")
for media in self.files:
cf.write(f"file '{ffmpeg_escape(media)}'\n")
duration_ms = self.get_duration_ms(media)
mf.write("[CHAPTER]\n")
mf.write("TIMEBASE=1/1000\n")
mf.write(f"START={current_ms}\n")
mf.write(f"END={current_ms + duration_ms}\n")
mf.write(f"title={media.stem}\n")
current_ms += duration_ms
cmd_backup = ["ffmpeg", "-y", "-f", "concat", "-safe", "0",
"-i", str(concat_file), "-i", str(metadata_file),
# cover from first media
"-i", str(self.firstFile),
# audio from concat
"-map", "0:a",
# cover image from first media
"-map", "2:v?",
# copy artist/album/composer/etc from first media file
"-map_metadata", "2",
# copy chapters from ffmetadata
"-map_chapters", "1",
"-c:a", "copy",
"-c:a", "aac",
"-b:a", "128k",
# copy cover image without re-encoding
"-c:v", "copy", "-disposition:v:0", "attached_pic",
str(output_file_tmp),
]
cmd_default = ["ffmpeg", "-y", "-f", "concat", "-safe", "0",
"-i", str(concat_file), "-i", str(metadata_file),
# cover from first media
"-i", str(self.firstFile),
# audio from concat
"-map", "0:a",
# cover image from first media
"-map", "2:v?",
# copy artist/album/composer/etc from first media file
"-map_metadata", "2",
# copy chapters from ffmetadata
"-map_chapters", "1",
"-c:a", "copy",
# copy cover image without re-encoding
"-c:v", "copy", "-disposition:v:0", "attached_pic",
str(output_file_tmp),
]
result = subprocess.run(
cmd_default,
capture_output=True,
text=True,
)
logger.info(result.stdout)
if result.returncode == 234:
logger.warning(
f"{YELLOW}[WARNING]{RESET} - {self.bookPath.name} - {result.returncode} retrying with aac encoder")
result = subprocess.run(
cmd_backup,
capture_output=True,
text=True,
)
logger.info(result.stdout)
if result.returncode == 0:
output_file_tmp.rename(output_file_final)
for media in self.files:
media.unlink()
self.files = Path(output_file_final)
self.firstFile = Path(output_file_final)
self.fileConunt = 1
self.singleFile = True
self.size = Path(output_file_final).stat().st_size
logger.info(f"{GREEN}[SUCCESS]{RESET} - {self.bookPath.name} - delete media files")
else:
logger.error(
f"{RED}[ERROR]{RESET} - {self.bookPath.name} - Code: {result.returncode}, Stderr: {result.stderr}")
def get_duration_ms(self,media: Path) -> int:
duration = subprocess.check_output(
[
"ffprobe",
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"default=noprint_wrappers=1:nokey=1",
str(media),
],
text=True,
).strip()
return int(float(duration) * 1000)
class AudiobookLibrary:
def __init__(self, libraryPath,logfilename: str="audiobook_library.log"):
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
handlers=[
logging.FileHandler(logfilename),
logging.StreamHandler()
]
)
logger.info('Started')
self.libraryPath = Path(libraryPath)
self.audiobooks = []
self._by_path = {}
self._by_name = defaultdict(list)
self._by_artist = defaultdict(list)
self._by_composer = defaultdict(list)
self.loadAudiobooksFromDir()
self.size = sum( audiobook.size for audiobook in self.audiobooks ) # size in bytes
def add(self, book: Audiobook):
self.audiobooks.append(book)
self._by_path[book.bookPath] = book
self._by_name[book.name].append(book)
if book.artist:
self._by_artist[book.artist].append(book)
if book.composer:
self._by_composer[book.composer].append(book)
def printName(self, books):
if not isinstance(books, list):
books = [books]
for book in books:
if book.name:
logger.info(book.name)
else:
logger.info(f"No name in {book.bookPath.name}")
def get_by_path(self, path):
return self._by_path.get(Path(path))
def get_by_name(self, name):
return self._by_name.get(name, [])
def get_by_artist(self, artist):
return self._by_artist.get(artist, [])
def get_by_composer(self, composer):
return self._by_artist.get(composer, [])
def find_name_contains(self, text):
text = text.lower()
return [
book
for book in self.audiobooks
if text in book.name.lower()
]
def find_artist_contains(self, text):
text = text.lower()
return [
book
for book in self.audiobooks
if book.artist and text in book.artist.lower()
]
def loadAudiobooksFromDir(self) -> tuple[list[Any], int]:
"""
A book directory is any directory containing more than one
audiobook file (.mp3, .m4a, .m4b)
"""
media_files = []
for ext in audio_extensions:
media_files.extend(list(self.libraryPath.rglob( ext)) )
dirs = {file.parent for file in media_files}
for directory in dirs:
self.add(Audiobook(directory))
def loadMetadata(self):
for audiobook in self.audiobooks:
audiobook.loadMetadata()
def fixComposers(self):
for book in self.audiobooks:
book.fixComposer()
def fixNames(self):
for book in self.audiobooks:
book.fixName()
def fixComments(self):
for book in self.audiobooks:
if len(book.comment) < 40:
logger.info(f"Remove short comment: {book.comment} of {book.name}")
book.comment = ""
def ffmpeg_escape(path: Path) -> str:
return str(path.resolve()).replace("'", r"'\''")
def main():
parser = argparse.ArgumentParser(
description="Audiobook Directory Processing"
)
parser.add_argument(
"library",
type=Path,
help="Library path"
)
args = parser.parse_args()
library_dir = args.library
library = AudiobookLibrary(library_dir)
library.fixComposers()
library.fixNames()
library.fixComments()
library.printName(library.get_by_artist("Astrid Lindgren"))
library.printName(library.find_artist_contains("Astrid Lindgren"))
library.printName(library.get_by_name("Pippi Langstrumpf"))
library.printName(library.find_name_contains("Pippi Langstrumpf"))
library.printName(library.get_by_composer("Heike Maklatsch"))
# library.printName(library.get_by_path("/home/lucas/Nextcloud/Audiobooks/Neu/Astrid Lindgren/Junker Nils von Eka (Hoerfest)"))
for audiobook in library.audiobooks:
first_file = audio_files[0]
artist = get_metadata("artist", first_file)
album = get_metadata("album", first_file)
composer = get_metadata("composer", first_file)
comment = get_metadata("comment", first_file)
print("====≠===============================")
print(f"Folder: {library_dir.name}")
print(f"Album: {album}")
print(f"Artist: {artist}")
print(f"Narrator: {composer}")
print(f"Comment: {comment}")
if __name__ == "__main__":
main()
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/python3
import json
import re
import sqlite3
import argparse
from pathlib import Path
def load_genre(conn, unique=False):
"""
Return all genres from the books table.
Args:
conn: sqlite3 connection
unique: if True, return unique genres only
Returns:
list[str]
"""
genres = []
for (genres_json,) in conn.execute("SELECT genres FROM books"):
if not genres_json:
continue
try:
genres.extend(json.loads(genres_json))
except Exception:
pass
if unique:
return sorted(set(genres))
return genres
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"-p", "--print-only",
action="store_true",
help="Print data only",
)
parser.add_argument(
"--print-all",
action="store_true",
help="Print all data only",
)
parser.add_argument(
"database",
type=Path,
help="SQL Database Path",
)
args = parser.parse_args()
database = args.database.resolve()
if not database.is_file():
parser.error(f"Database is not a file: {database}")
db = sqlite3.connect(database)
cur = db.cursor()
list_genres = load_genre(cur,True)
if args.print_all:
for genre in list_genres:
print(genre)
return
rows = cur.execute(
"SELECT id, genres FROM books"
).fetchall()
for book_id, genres_json in rows:
if not genres_json:
continue
genres = json.loads(genres_json)
genres_new = []
new_genres = []
print("=================================")
print(f"Before sed: {genres}")
for genre in genres:
if "Drachen" in genre:
pass
if "Fantastic" in genre:
pass
genre = re.sub(r'(?i)^audio\s*books?$', '', genre)
genre = re.sub(r'(?i)^audiobooks?$', '', genre)
genre = re.sub(r'(?i)\b(hörb(uch|ücher)|hoerb(uch|uecher))\b', '', genre)
genre = re.sub(r'(?i)^(hörspiele?|hoerspiele?)$', '', genre)
genre = re.sub(r'(?i)^spoken\s*word$', '', genre)
genre = re.sub(r'(?i)gesprochene\s*inhalte$', '', genre)
genre = re.sub(r'(?i)^speech$', '', genre)
genre = re.sub(r'Fanasie', 'Fantasie', genre)
genre = re.sub(r'Mit Videospiel', 'Videospielbezug', genre)
genre = re.sub(r'(?i)nonfiction', 'NonFiction', genre)
genre = re.sub(r'(?i)^speech$', '', genre)
genre = re.sub(r'(?i)^vocal$', '', genre)
genre = re.sub(r'(?i)^retail$', '', genre)
genre = re.sub(r'(?i)^other$', '', genre)
genre = re.sub(r'(?i)^misc$', '', genre)
genre = re.sub(r'(?i)^(sci[- ]?fi|sf)$', 'Science Fiction', genre)
genre = re.sub(r'(?i)^\(?science fiction\)?$', 'Science Fiction', genre)
genre = re.sub(r'Thrillers', 'Thriller', genre)
genre = re.sub(r"(?i)\bchildren'?s audiobooks?\b", "",genre)
genre = re.sub(r'^-\s*|\s*-$', '', genre)
genre = re.sub(r'(?i)Engelske\s*\sLydbånd','',genre)
genre = re.sub(r'^FIC028000.*$', '', genre)
genre = re.sub(r'^800.*$', '', genre)
genre = re.sub(r'^Fantastic Beasts.*$', '', genre)
genre = re.sub(r'^and thei.*$', '', genre)
genre = re.sub(r'^those you.*$', '', genre)
genre = genre.strip()
genre = genre.lstrip()
if "," in genre:
genre = genre.split(",")
elif " - " in genre:
genre = genre.split(" - ")
elif "Historical Adventure Thriller Mystery" in genre:
genre = genre.split(" ")
if isinstance(genre, str):
if len(genre) > 50 or (5 > len(genre) > 0):
genre = ""
print(f"Too long or too short: {genre}")
genre = genre.strip()
genre = genre.lstrip()
new_genres.append(genre)
elif isinstance(genre, list):
if len(genres) > 1:
genres = [g for g in genres if g]
new_genres.extend(genre)
new_genres = sorted(set(new_genres))
print(f"After sed: {new_genres}")
list_difference = False
print("Write to Database")
if len(new_genres) < len(genres):
list_difference = True
for old,new in zip(genres,new_genres):
if old != new:
list_difference = True
break
if not list_difference:
print("No difference: Dont Commit to Database")
continue
if args.print_only:
print("Print only: Dont Commit to database")
continue
print("Write to Database")
print("=================================")
cur.execute(
"UPDATE books SET genres=? WHERE id=?",
(json.dumps(new_genres), book_id),
)
print("Database Updated")
db.commit()
if __name__ == "__main__":
main()
@@ -0,0 +1,192 @@
#!/usr/bin/env python3
import argparse
import re
import subprocess
from pathlib import Path
from datetime import datetime
def get_genre(file_path):
try:
result = subprocess.run(
["kid3-cli", "-c", "get genre", str(file_path)],
capture_output=True,
text=True,
check=False,
)
return result.stdout.strip()
except Exception:
return ""
def set_genre(file_path, genre):
subprocess.run(
[
"kid3-cli",
"-c",
"tag 2",
"-c",
f"set genre \"{genre}\"",
"-c",
"save",
str(file_path),
],
check=False,
)
def clean_genre(genre):
genre = re.sub(r'(?i)^audio\s*books?$', '', genre)
genre = re.sub(r'(?i)^audiobooks?;?$', '', genre)
genre = re.sub(r'(?i)\b(hörb(uch|ücher)|hoerb(uch|uecher))\b', '', genre)
genre = re.sub(r'(?i)^(hörspiele?|hoerspiele?)$', '', genre)
genre = re.sub(r'(?i)^spoken\s*word$', '', genre)
genre = re.sub(r'(?i)gesprochene\s*inhalte$', '', genre)
genre = re.sub(r'(?i)^speech$', '', genre)
genre = re.sub(r'Fanasie', 'Fantasie', genre)
genre = re.sub(r'Mit Videospiel-?', 'Videospielbezug', genre)
genre = re.sub(r'(?i)nonfiction', 'NonFiction', genre)
genre = re.sub(r'(?i)^speech$', '', genre)
genre = re.sub(r'(?i)^vocal$', '', genre)
genre = re.sub(r'(?i)^retail$', '', genre)
genre = re.sub(r'(?i)^other$', '', genre)
genre = re.sub(r'(?i)^misc$', '', genre)
genre = re.sub(r'(?i)^(sci[- ]?fi|sf)$', 'Science Fiction', genre)
genre = re.sub(r'(?i)^\(?science fiction\)?$', 'Science Fiction', genre)
genre = re.sub(r'Thrillers', 'Thriller', genre)
genre = re.sub(r"(?i)\bchildren'?s audiobooks?\b", "", genre)
genre = re.sub(r'^-\s*|\s*-$', '', genre)
genre = re.sub(r'(?i)Engelske\s*\sLydbånd', '', genre)
genre = re.sub(r'^FIC028000.*$', '', genre)
genre = re.sub(r'^800.*$', '', genre)
genre = re.sub(r'^Fantastic Beasts.*$', '', genre)
genre = re.sub(r'^and thei.*$', '', genre)
genre = re.sub(r'^those you.*$', '', genre)
# Normalize separators
genre = re.sub(
r'\s*[,&:/]\s*',
';',
genre
)
genre = re.sub(
r'\s*;\s*',
'; ',
genre
)
# Trim punctuation / whitespace
genre = re.sub(
r'^[\s,;:\-]+',
'',
genre
)
genre = re.sub(
r'[\s,;\-]+$',
'',
genre
)
genre = genre.strip()
genre = genre.lstrip()
return genre
def find_audio_files(library):
extensions = {".mp3", ".m4a", ".m4b"}
return [
p for p in Path(library).rglob("*")
if p.is_file() and p.suffix.lower() in extensions
]
def main():
parser = argparse.ArgumentParser(
description="Audiobook Genre Cleanup"
)
parser.add_argument(
"library",
help="Library path"
)
parser.add_argument(
"--delete",
action="store_true",
help="Update tags instead of reporting only"
)
parser.add_argument(
"--report",
default="/home/lucas/audiobook_genre_cleanup.txt",
help="Report file"
)
args = parser.parse_args()
files = find_audio_files(args.library)
total = len(files)
processed = 0
matches = 0
modified = 0
with open(args.report, "w", encoding="utf-8") as report:
report.write("Audiobook Genre Cleanup Report\n")
report.write(f"Generated: {datetime.now()}\n")
report.write(f"Library: {args.library}\n")
report.write(f"Total files: {total}\n\n")
for file in files:
processed += 1
if processed % 100 == 0:
print(f"Progress: {processed}/{total} files processed")
genre = get_genre(file)
if not genre:
continue
cleaned = clean_genre(genre)
if genre != cleaned:
matches += 1
status = "MATCH"
if args.delete:
set_genre(file, cleaned)
modified += 1
status = "UPDATED"
line = (
f'[{processed}/{total}] [{status}] '
f'{file} | OLD="{genre}" | NEW="{cleaned}"'
)
print(line)
with open(args.report, "a", encoding="utf-8") as report:
report.write(line + "\n")
with open(args.report, "a", encoding="utf-8") as report:
report.write("\n========== SUMMARY ==========\n")
report.write(f"Files scanned : {total}\n")
report.write(f"Matches found : {matches}\n")
report.write(f"Files updated : {modified}\n")
print("\n========== SUMMARY ==========")
print(f"Files scanned : {total}")
print(f"Matches found : {matches}")
print(f"Files updated : {modified}")
print(f"Report written to: {args.report}")
if __name__ == "__main__":
main()
-27
View File
@@ -1,27 +0,0 @@
#!/usr/bin/env bash
ROOT="${1:-.}"
find "$ROOT" -type d | while read -r dir; do
first_file=$(
find "$dir" -maxdepth 1 -type f \
\( -iname "*.m4b" -o -iname "*.m4a" -o -iname "*.mp3" \
-o -iname "*.flac" -o -iname "*.ogg" \) |
sort |
head -n1
)
[[ -z "$first_file" ]] && continue
artist=$(kid3-cli -c "get artist" "$first_file" 2>/dev/null)
album=$(kid3-cli -c "get album" "$first_file" 2>/dev/null)
composer=$(kid3-cli -c "get composer" "$first_file" 2>/dev/null)
comment=$(kid3-cli -c "get comment" "$first_file" 2>/dev/null)
echo "====≠==============================="
echo "Folder: $(basename "$dir")"
echo "Album: $album"
echo "Artist: $artist"
echo "Narrator: $composer"
echo "Comment: $comment"
done
@@ -27,7 +27,7 @@ fi
list="$1"
while IFS= read -r file; do
book
[[ -z "$file" ]] && continue
if [[ ! -e "$file" ]]; then
+2 -2
View File
@@ -41,8 +41,8 @@ for file in "${FILES[@]}"; do
[[ -z "$genre" ]] && continue
cleaned=$(printf '%s\n' "$genre" | sed -E '
s/[Aa]udio[[:space:]]*[Bb]ooks?//g
s/\b(Hörb(uch|ücher)|Hoerb(uch|uecher)|Hörspiel(e)?|Hoerspiel(e)?)\b//Ig
s/-?(Hörb(uch|ücher)|Hoerb(uch|uecher)|Hörspiel(e)?|Hoerspiel(e)?)\b//Ig
s/[Gg]esprochene[[:space:]]*[Ii]nhalte//g
s/[Ss]poken[[:space:]]*[Ww]ord//g
s/[Ss]peech//g
@@ -16,4 +16,10 @@ services:
- /data/gitea:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
networks:
- default
- shared-proxy-services
networks:
default:
shared-proxy-services:
external: true
@@ -9,6 +9,11 @@ services:
TZ: Europe/Vienna
volumes:
- /srv/homeassistant:/config
devices:
- /dev/bus/usb:/dev/bus/usb
cap_add:
- NET_ADMIN
- NET_RAW
mosquitto:
container_name: mosquitto
@@ -0,0 +1,81 @@
#
# WARNING: To install Immich, follow our guide: https://docs.immich.app/install/docker-compose
#
# Make sure to use the docker-compose.yml of the current release:
#
# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
#
# The compose file on main may not be compatible with the latest release.
name: immich
services:
immich-server:
container_name: immich_server
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
# extends:
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
volumes:
# Do not edit the next line. If you want to change the media storage location on your system, edit the value of UPLOAD_LOCATION in the .env file
- ${UPLOAD_LOCATION}:/data
- /data/immich/pictures:/libary
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
ports:
- '2283:2283'
depends_on:
- database
restart: always
healthcheck:
disable: false
networks:
- immich
- shared-redis-services
immich-machine-learning:
container_name: immich_machine_learning
# For hardware acceleration, add one of -[armnn, cuda, rocm, openvino, rknn] to the image tag.
# Example tag: ${IMMICH_VERSION:-release}-cuda
image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
# extends: # uncomment this section for hardware acceleration - see https://docs.immich.app/features/ml-hardware-acceleration
# file: hwaccel.ml.yml
# service: cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl, rknn] for accelerated inference - use the `-wsl` version for WSL2 where applicable
volumes:
- model-cache:/cache
env_file:
- .env
restart: always
healthcheck:
disable: false
networks:
- immich
database:
container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
POSTGRES_INITDB_ARGS: '--data-checksums'
# Uncomment the DB_STORAGE_TYPE: 'HDD' var if your database isn't stored on SSDs
# DB_STORAGE_TYPE: 'HDD'
volumes:
# Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
shm_size: 128mb
restart: always
healthcheck:
disable: false
networks:
- immich
volumes:
model-cache:
networks:
immich:
shared-redis-services:
external: true
@@ -15,7 +15,6 @@ services:
networks:
- default
- shared-proxy-services
seerr:
image: ghcr.io/seerr-team/seerr:latest
container_name: seerr
@@ -29,6 +28,7 @@ services:
networks:
- default
- shared-proxy-services
networks:
default:
external: true
@@ -1,22 +1,5 @@
services:
sonarr-en:
image: lscr.io/linuxserver/sonarr:latest
container_name: sonarr-en
restart: unless-stopped
ports:
- "8989:8989"
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Berlin
volumes:
- ./sonarr_EN:/config
- /data/jellyfin/media/series-english:/JellyfinSeriesEnglish
- /data/jellyfin/media/anime-english:/JellyfinAnimeEnglish
- /data/jellyfin/media/documentaries-english:/JellyfinDocus
- /data/jellyfin/buffer:/buffer
- /torrent/qbittorrent/downloads:/downloads
sonarr-de:
image: lscr.io/linuxserver/sonarr:latest
container_name: sonarr-de
@@ -31,10 +14,13 @@ services:
- ./sonarr_DE:/config
- /data/jellyfin/media/series-german:/JellyfinSeriesGerman
- /data/jellyfin/media/anime-german:/JellyfinAnimeGerman
- /data/jellyfin/media/series-english:/JellyfinSeriesEnglish
- /data/jellyfin/media/anime-english:/JellyfinAnimeEnglish
- /data/jellyfin/media/documentaries-english:/JellyfinDocus
- /data/jellyfin/media/series-kids:/JellyfinKinder
- /data/jellyfin/buffer:/buffer
- /torrent/qbittorrent/downloads:/downloads
- /torrent/rundfunkarr:/downloads-rundfunkarr
- /torrent/rundfunkarr-de:/downloads-rundfunkarr
radarr-en:
image: lscr.io/linuxserver/radarr:latest
container_name: radarr-en
@@ -67,7 +53,7 @@ services:
- /data/jellyfin/media/movies-kids:/JellyfinKinder
- /data/jellyfin/buffer:/buffer
- /torrent/qbittorrent/downloads:/downloads
- /torrent/rundfunkarr:/downloads-rundfunkarr
- /torrent/rundfunkarr-de:/downloads-rundfunkarr
prowlarr:
image: lscr.io/linuxserver/prowlarr:latest
container_name: prowlarr
@@ -177,7 +163,7 @@ services:
- TZ=Europe/Berlin
volumes:
- ./lidarr:/config
- /data/navidrome/music:/music
- /data/navidrome/music/music:/music
- /torrent/qbittorrent/downloads:/downloads
ports:
@@ -261,14 +247,43 @@ services:
- /data/jellyfin/media/comedy-english:/data/comedy-english:ro
- /data/jellyfin/media/movies-german:/data/movies-german:ro
- /data/jellyfin/media/movies-kids:/data/movies-kids:ro
# - /data/audiobookshelf/podcasts:/podcasts
# - /data/audiobookshelf/audiobooks-englisch:/audiobooks
- /data/navidrome/music:/data/music:ro
- /data/audiobookshelf/data/audiobooks-englisch:/data/audiobooks-english:ro
- /data/audiobookshelf/data/audiobooks-deutsch:/data/audiobooks-german:ro
- /data/audiobookshelf/data/audiobooks-kinder:/data/audiobooks-kinder:ro
# Download location
- /torrent/qbittorrent/downloads:/downloads
jackett:
image: lscr.io/linuxserver/jackett:latest
container_name: jackett
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Vienna
volumes:
- ./jackett/config:/config
- ./jackett/downloads:/downloads
ports:
- "9117:9117"
restart: unless-stopped
gluetun-proxy:
image: qmcgaw/gluetun:latest
container_name: gluetun-proxy
cap_add:
- NET_ADMIN
ports:
- "8889:8888" # HTTP proxy
- "8388:8388" # SOCKS5 proxy
volumes:
- ./gluetun-proxy:/gluetun/wireguard
- /dev/net/tun:/dev/net/tun
environment:
- VPN_SERVICE_PROVIDER=custom
- VPN_TYPE=wireguard
- SOCKS5_ENABLED=on
- HTTPPROXY=on
- SOCKSPROXY=on
restart: unless-stopped
networks:
default:
@@ -0,0 +1,11 @@
#
# Use this configuration with WireGuard client
#
[Interface]
Address = 10.14.0.2/16
PrivateKey = mKeDQdxtM1iFJS7WLlb2jPGabZIt3NkoA/nGCcNOU1I=
DNS = 162.252.172.57, 149.154.159.92
[Peer]
PublicKey = dPZe8Jq3Hu0k07MDk+Y4+AS2XHSLYalyg91TSFXRYEA=
AllowedIPs = 0.0.0.0/0
Endpoint = 87.249.133.4:51820
@@ -0,0 +1,8 @@
modules:
http_2xx:
prober: http
timeout: 5s
http:
valid_status_codes: []
method: GET
preferred_ip_protocol: ip4
@@ -8,6 +8,9 @@ services:
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
networks:
- default
- shared-proxy-services
grafana:
image: grafana/grafana:latest
@@ -47,9 +50,9 @@ services:
volumes:
- '/:/host:ro,rslave'
- ./node_exporter/textfile_collector:/textfile:ro
ports:
- '9100:9100'
# ports:
# - '9100:9100'
network_mode: host
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
container_name: cadvisor
@@ -58,9 +61,24 @@ services:
- "8080:8080"
volumes:
- /:/rootfs:ro
- /data:/data:ro
- /docker:/docker:ro
# - /data:/data:ro
# - /docker:/docker:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker:/var/lib/docker:ro
command:
- "--disable_metrics=percpu,tcp,udp,process,sched"
- "--housekeeping_interval=30s"
- "--housekeeping_interval=1m"
- "--docker_only=true"
blackbox-exporter:
image: prom/blackbox-exporter:latest
container_name: blackbox-exporter
restart: unless-stopped
ports:
- "9115:9115"
volumes:
- ./blackbox.yml:/etc/blackbox_exporter/config.yml:ro
volumes:
prometheus-data:
grafana-data:
@@ -1,6 +1,6 @@
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_interval: 30s
evaluation_interval: 30s
scrape_configs:
@@ -8,6 +8,35 @@ scrape_configs:
static_configs:
- targets:
- "prometheus:9090"
- job_name: "nginx"
static_configs:
- targets:
- nginx-exporter:9113
- job_name: blackbox_https
metrics_path: /probe
params:
module: [http_2xx]
static_configs:
- targets:
- https://cloud.someoneslinux.selfhost.eu
- https://media.someoneslinux.selfhost.eu
- https://audiobooks.someoneslinux.selfhost.eu
# - https://login.someoneslinux.selfhost.eu
- https://auto.someoneslinux.selfhost.eu
- https://music.someoneslinux.selfhost.eu
- https://passwd.someoneslinux.selfhost.eu
- https://requests.someoneslinux.selfhost.eu
- https://torrent.someoneslinux.selfhost.eu
- https://status.someoneslinux.selfhost.eu
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:9115
- job_name: "node-exporter"
static_configs:
@@ -16,20 +45,33 @@ scrape_configs:
labels:
host: serverhost
- targets:
- "node-exporter:9100"
- "192.168.0.4:9100"
labels:
host: "mediahost"
- targets:
- "192.168.0.39:9100"
labels:
host: "quiethost"
- job_name: "cadvisor"
static_configs:
- targets:
- "cadvisor:8080"
labels:
host: mediahost
- targets:
- "192.168.0.2:8081"
labels:
host: serverhost
- job_name: "smartctl"
static_configs:
- targets:
- "192.168.0.2:9633"
labels:
host: serverhost
- targets:
- "192.168.0.39:9633"
labels:
host: "quiethost"
- targets:
- "192.168.0.4:9633"
labels:
@@ -23,6 +23,7 @@ services:
- /data/navidrome/music/kamerun:/kamerun:ro
- /data/navidrome/music/weihnachten:/weihnachten:ro
- /data/navidrome/music/playlists:/playlists:ro
- /data/navidrome/music/playlists-export:/playlists-export
networks:
- shared-proxy-services
- default
@@ -17,7 +17,8 @@ server {
passwd.someoneslinux.selfhost.eu
requests.someoneslinux.selfhost.eu
torrent.someoneslinux.selfhost.eu
status.someoneslinux.selfhost.eu;
status.someoneslinux.selfhost.eu
code.someoneslinux.selfhost.eu;
#ssl_conf_command Groups X25519:secp256r1:secp384r1;
# ssl_conf_command Groups X25519:prime256v1:secp384r1;
location /.well-known/acme-challenge/ {
@@ -27,12 +28,28 @@ server {
location / {
return 301 https://$host$request_uri;
}
listen 8080;
location /nginx_status {
stub_status;
allow 172.16.0.0/12;
deny all;
}
}
# Common SSL settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
server {
listen 443 ssl default_server;
server_name _;
ssl_certificate /etc/nginx/certs/live/cloud.someoneslinux.selfhost.eu/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/live/cloud.someoneslinux.selfhost.eu/privkey.pem;
return 404;
}
###################
# NEXTCLOUD
@@ -51,6 +68,8 @@ server {
location / {
proxy_pass http://nextcloud:80;
# Do not try another upstream on errors
proxy_next_upstream off;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@@ -76,6 +95,8 @@ server {
location / {
proxy_pass http://jellyfin:8096;
# Do not try another upstream on errors
proxy_next_upstream off;
proxy_http_version 1.1;
proxy_set_header Host $host;
@@ -103,6 +124,8 @@ server {
location / {
proxy_pass http://audiobookshelf:80;
# Do not try another upstream on errors
proxy_next_upstream off;
proxy_http_version 1.1;
proxy_set_header Host $host;
@@ -112,40 +135,14 @@ server {
}
}
###################
# AUTHENTIK
###################
server {
listen 443 ssl;http2 on;
server_name login.someoneslinux.selfhost.eu;
add_header Content-Security-Policy "frame-ancestors 'self';" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
ssl_certificate /etc/nginx/certs/live/cloud.someoneslinux.selfhost.eu/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/live/cloud.someoneslinux.selfhost.eu/privkey.pem;
location / {
proxy_pass http://authentik-server-1:9000;
proxy_http_version 1.1;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
####################
## HOME ASSISTANT
## AUTHENTIK
####################
#server {
# listen 443 ssl;http2 on;
# server_name auto.someoneslinux.selfhost.eu;
# server_name login.someoneslinux.selfhost.eu;
#
# add_header Content-Security-Policy "default-src 'self'; frame-ancestors 'self';" always;
# add_header Content-Security-Policy "frame-ancestors 'self';" always;
# add_header X-Frame-Options "SAMEORIGIN" always;
# add_header X-Content-Type-Options "nosniff" always;
# add_header Referrer-Policy "strict-origin-when-cross-origin" always;
@@ -155,17 +152,45 @@ server {
# ssl_certificate_key /etc/nginx/certs/live/cloud.someoneslinux.selfhost.eu/privkey.pem;
#
# location / {
# proxy_pass http://192.168.0.4:8123;
# proxy_pass http://authentik-server-1:9000;
# proxy_http_version 1.1;
#
# proxy_set_header Host $host;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection $connection_upgrade;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
#
# proxy_set_header X-Forwarded-Proto https;
# proxy_set_header X-Forwarded-Host $host;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# }
#}
###################
# HOME ASSISTANT
###################
server {
listen 443 ssl;http2 on;
server_name auto.someoneslinux.selfhost.eu;
# add_header Content-Security-Policy "default-src 'self'; frame-ancestors 'self';" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
ssl_certificate /etc/nginx/certs/live/cloud.someoneslinux.selfhost.eu/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/live/cloud.someoneslinux.selfhost.eu/privkey.pem;
location / {
proxy_pass http://192.168.0.4:8123;
# Do not try another upstream on errors
proxy_next_upstream off;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
}
###################
# NAVIDROME
###################
@@ -184,6 +209,8 @@ server {
location / {
proxy_pass http://navidrome:4533;
# Do not try another upstream on errors
proxy_next_upstream off;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Real-IP $remote_addr;
@@ -208,6 +235,8 @@ server {
location / {
proxy_pass http://vaultwarden:80;
# Do not try another upstream on errors
proxy_next_upstream off;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
@@ -216,6 +245,8 @@ server {
location /notifications/hub {
proxy_pass http://vaultwarden:3012;
# Do not try another upstream on errors
proxy_next_upstream off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
@@ -240,6 +271,8 @@ server {
location / {
proxy_pass http://seerr:5055;
# Do not try another upstream on errors
proxy_next_upstream off;
proxy_http_version 1.1;
proxy_set_header Host $host;
@@ -268,6 +301,8 @@ server {
location / {
proxy_pass http://192.168.0.2:8080;
# Do not try another upstream on errors
proxy_next_upstream off;
proxy_http_version 1.1;
proxy_set_header Host $host;
@@ -275,6 +310,7 @@ server {
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-Forwarded-Proto https;
}
}
###################
@@ -294,6 +330,8 @@ server {
location / {
proxy_pass http://grafana:3000;
# Do not try another upstream on errors
proxy_next_upstream off;
proxy_http_version 1.1;
proxy_set_header Host $host;
@@ -302,3 +340,33 @@ server {
proxy_set_header X-Forwarded-Proto https;
}
}
###################
# GITEA
###################
server {
listen 443 ssl;http2 on;
server_name code.someoneslinux.selfhost.eu;
add_header Content-Security-Policy "frame-ancestors 'self';" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
ssl_certificate /etc/nginx/certs/live/cloud.someoneslinux.selfhost.eu/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/live/cloud.someoneslinux.selfhost.eu/privkey.pem;
location / {
proxy_pass http://gitea:3000;
# Do not try another upstream on errors
proxy_next_upstream off;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
@@ -3,11 +3,9 @@ services:
image: nginx:stable
container_name: nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./conf.d:/etc/nginx/conf.d
- ./certs:/etc/nginx/certs
@@ -15,8 +13,23 @@ services:
# Access and error logs
- ./logs:/var/log/nginx
networks:
- default
- shared-proxy-services
nginx-exporter:
image: nginx/nginx-prometheus-exporter:latest
container_name: nginx-exporter
restart: unless-stopped
command:
- --nginx.scrape-uri=http://nginx:8080/nginx_status
ports:
- "9113:9113"
depends_on:
- nginx
networks:
- default
- shared-proxy-services
crowdsec:
image: crowdsecurity/crowdsec:latest
container_name: crowdsec
@@ -29,7 +42,8 @@ services:
- crowdsec-db:/var/lib/crowdsec/data
- ./logs:/var/log/nginx:ro
- /etc/localtime:/etc/localtime:ro
networks:
- default
certbot:
image: certbot/certbot:latest
@@ -41,7 +55,9 @@ services:
- shared-proxy-services
networks:
default:
shared-proxy-services:
external: true
volumes:
crowdsec-db:
@@ -3,7 +3,7 @@ services:
image: redis:7
container_name: redis
restart: unless-stopped
command: redis-server --appendonly yes --requirepass PerCent7heStorySp0tInOrd3r
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
healthcheck:
test:
@@ -10,6 +10,19 @@ services:
volumes:
- '/:/host:ro,rslave'
- ./node_exporter/textfile_collector:/textfile:ro
# ports:
# - '9100:9100'
network_mode: host
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
container_name: cadvisor
restart: unless-stopped
ports:
- '9100:9100'
- "8081:8080"
volumes:
- /:/rootfs:ro
- /data:/data:ro
- /docker:/docker:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker:/var/lib/docker:ro
@@ -72,8 +72,8 @@ fi
TORRENT_NAME="$1"
ROOT_PATH="$2"
CATEGORY="$3"
if [[ "$CATEGORY" != *arr* && "$CATEGORY" != "mytorrents" ]]; then
if [[ "$CATEGORY" != *arr* ]]; then
###############################################################################
# Start
###############################################################################
@@ -155,7 +155,7 @@ if [[ "$CATEGORY" != *arr* ]]; then
lucas@mediahost "mkdir -p \"/torrent/qbittorrent/mediahost/$remote_dir\""
SSH_STATUS=$?
scp -i /config/.ssh/id_ed25519 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-r "$ROOT_PATH"/. lucas@mediahost:/torrent/qbittorrent/mediahost/
-r "$ROOT_PATH"/. lucas@mediahost:/torrent/qbittorrent/mediahost/$remote_dir/
SCP_STATUS=$?
echo "scp exit status: $SCP_STATUS"
+170
View File
@@ -0,0 +1,170 @@
#!/usr/bin/env python3
import re
import sys
from pathlib import Path
from difflib import SequenceMatcher
def normalize(name):
name = Path(name).stem.lower()
# remove brackets
name = re.sub(r"\(.*?\)|\[.*?\]", "", name)
# replace punctuation with spaces
name = re.sub(r"[^a-z0-9]", " ", name)
# collapse whitespace
name = " ".join(name.split())
return name
def similarity(a, b):
words_a = set(normalize(a).split())
words_b = set(normalize(b).split())
if not words_a or not words_b:
return 0.0
# Jaccard similarity
intersection = words_a & words_b
union = words_a | words_b
return len(intersection) / len(union)
def load_playlist(path):
entries = []
with open(path, encoding="utf-8", errors="ignore") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
entries.append(Path(line).name)
return entries
def compare_playlists(orig_file, new_file, threshold=0.85):
orig_files = load_playlist(orig_file)
new_files = load_playlist(new_file)
print(f"\n=== {orig_file.parent} ===")
print(f"Comparing:")
print(f" {orig_file.name}")
print(f" {new_file.name}")
missing = []
for orig in orig_files:
best_match = None
best_score = 0
for candidate in new_files:
score = similarity(orig, candidate)
if score > best_score:
best_score = score
best_match = candidate
if best_score >= threshold:
pass
# print(
# f"MATCH {best_score:.1%} "
# f"{orig} -> {best_match}"
# )
else:
missing.append((orig, best_score, best_match))
if missing:
print("\nMISSING:")
for orig, score, best_match in sorted(
missing,
key=lambda x: x[1],
reverse=True,
):
print(
f" {orig}"
f" (best={score:.1%}"
f"{', candidate=' + best_match if best_match else ''})"
)
return len(missing)
def compare_directory(root, threshold=0.85):
total_missing = 0
playlist_pairs = 0
for orig_file in root.rglob("*_orig.m3u"):
new_file = orig_file.with_name(
orig_file.name.replace("_orig.m3u", ".m3u")
)
if not new_file.exists():
print(
f"\nWARNING: Missing playlist for "
f"{orig_file}"
)
continue
playlist_pairs += 1
total_missing += compare_playlists(
orig_file,
new_file,
threshold,
)
print("\n" + "=" * 80)
print(f"Playlists compared : {playlist_pairs}")
print(f"Total missing : {total_missing}")
def main():
args = sys.argv[1:]
threshold = 0.85
if len(args) == 3 and args[0] == "--threshold":
threshold = float(args[1])
args = [args[2]]
elif len(args) == 4 and args[0] == "--threshold":
threshold = float(args[1])
args = [args[2], args[3]]
if len(args) == 1:
path = Path(args[0])
if not path.is_dir():
print("Single argument must be a directory")
sys.exit(1)
compare_directory(path, threshold)
elif len(args) == 2:
compare_playlists(
Path(args[0]),
Path(args[1]),
threshold,
)
else:
print(
f"Usage:\n"
f" {Path(sys.argv[0]).name} "
f"<playlist_orig.m3u> <playlist.m3u>\n\n"
f" {Path(sys.argv[0]).name} "
f"<directory>\n\n"
f"Optional:\n"
f" --threshold 0.90"
)
sys.exit(1)
if __name__ == "__main__":
main()
+8 -5
View File
@@ -167,19 +167,22 @@ process_file() {
HOSTNAME_EXPECTED="mediahost"
SOURCE_DIR="/data/nextcloud/syncuser/alina/Dropfolder"
TARGET_DIR="/data/nextcloud/syncuser/camera"
SOURCE_DIR="/data/nextcloud/syncuser/pictures/Alina Share/Dropfolder"
TARGET_DIR="/data/nextcloud/syncuser/pictures/Camera"
# If running on serverhost, perform initial sync
if [[ "$(hostname)" == "$HOSTNAME_EXPECTED" ]]; then
syncNC -d -c
echo Sync Nextcloud
syncNC -c
cd "$SOURCE_DIR" || exit 1
fi
if [[ $# -gt 0 ]]; then
echo Process single file $1
process_file "$1"
else
echo Process dir .
shopt -s nullglob
files=( *.heic *.mov )
@@ -202,7 +205,7 @@ if [[ "$(hostname)" == "$HOSTNAME_EXPECTED" ]]; then
fi
done
shopt -u nullglob
syncNC -d -c
echo Sync Nextcloud
syncNC -c
fi
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash
OUT="/docker/monitoring/node_exporter/textfile_collector/smart_selftest.prom"
> "$OUT"
for dev in $(smartctl --scan-open | awk '{print $1}'); do
name=$(basename "$dev")
LAST=0
if [[ "$dev" == *nvme* ]]; then
# POWER_ON=$(smartctl -a "$dev" | awk -F': *' '/Power On Hours:/ {print $2}')
LAST=$(smartctl -l selftest "$dev" 2>/dev/null \
| grep "Completed" \
| head -1 \
| awk '{print $(NF-5)}')
elif [[ "$dev" == *sd* ]]; then
# POWER_ON=$(smartctl -a "$dev" | awk '/Power_On_Hours/ {print $10}')
LAST=$(smartctl -l selftest "$dev" 2>/dev/null \
| grep "Completed" \
| head -1 \
| awk '{print $(NF-1)}')
else
POWER_ON=99999999999
echo $dev
fi
# Skip devices without self-test history
echo $name: ${LAST}
echo "smart_last_selftest_poweron_hours{device=\"$name\"} $LAST" >> "$OUT"
done
+7 -1
View File
@@ -2,7 +2,13 @@
OUT=/docker/monitoring/node_exporter/textfile_collector/custom.prom
TS=$(date +%s%3N)
DOCKER_CONTAINER_NUM=44
if [[ "$(hostname)" == "mediahost" ]]; then
DOCKER_CONTAINER_NUM=44
elif [[ "$(hostname)" == "serverhost" ]]; then
DOCKER_CONTAINER_NUM=4
elif [[ "$(hostname)" == "quiethost" ]]; then
DOCKER_CONTAINER_NUM=1
fi
{
# Docker container states
docker ps -a --format '{{.Names}}' | while read -r name; do
+2
View File
@@ -2,6 +2,8 @@
device=$1
if [ "$device" == "serverhost" ]; then
mac="64:51:06:51:cb:39"
elif [ "$device" == "mediahost" ]; then
mac="9c:6b:00:a3:36:c7"
elif [ "$device" == "quiethost" ]; then
mac="24:4B:FE:DE:93:3E"
elif [ "$(echo ${device} | grep -E -o "([0-9A-Fa-f]{2}[\:]){5}[0-9A-Fa-f]{2}")" != "" ]; then
+2 -21
View File
@@ -27,7 +27,7 @@ LUCAS_LOG="$LOGDIR/log_occ_lucas_${DATE}.log"
MULTI_LOG="$LOGDIR/log_occ_multi_${DATE}.log"
NC_PATH="/nextcloud/nextcloud"
OCC="sudo -u www-data php $NC_PATH/occ"
OCC="docker exec -u www-data nextcloud php occ"
STARTTIME=$(date +"%F %T")
@@ -41,17 +41,6 @@ echo "===================================================="
echo "Nextcloud maintenance started: $STARTTIME"
echo "===================================================="
cleanup() {
echo
echo "Starting Apache..."
sudo systemctl start apache2
}
trap cleanup EXIT
echo "Stopping Apache..."
sudo systemctl stop apache2
###############################################################################
# Cleanup
###############################################################################
@@ -68,7 +57,7 @@ echo
echo "Starting file scans..."
{
$OCC files:scan -vvv mimi >> "$MIMI_LOG" 2>&1
$OCC files:scan -vvv Mimi >> "$MIMI_LOG" 2>&1
echo "$(date): Mimi finished" >> "$MIMI_LOG"
} &
PID1=$!
@@ -79,16 +68,8 @@ PID1=$!
} &
PID2=$!
{
$OCC files:scan -vvv multimedia >> "$MULTI_LOG" 2>&1
echo "$(date): Multimedia finished" >> "$MULTI_LOG"
} &
PID3=$!
wait "$PID1"
wait "$PID2"
wait "$PID3"
echo "All scans completed."
###############################################################################
+1 -18
View File
@@ -25,7 +25,6 @@ SYNC_MEMES=1
SYNC_AUDIOBOOKS=1
SYNC_RECIPIES=1
SYNC_ALINA=1
SYNC_ALINA_DROP=1
SYNC_WALLPAPER=1
SYNC_PICTURES=1
SYNC_SYNCFOLDER=1
@@ -39,13 +38,12 @@ Synchronize configured directories with Nextcloud.
Options:
-h, --help Show this help message and exit
-l Show synchronization output
-d Enable Alina Share synchronization
-c Enable Pictures synchronization
-w Enable Wallpaper synchronization
-m Enable Memes synchronization
-s Enable Syncfolder synchronization
-ab Enable Audiobook synchronization
-al Enable Alina synchronization
-al Enable Alina Documents synchronization
-all Enable all synchronization operations
-del Remove the Nextcloud sync marker
--version Show script version
@@ -109,11 +107,6 @@ parse_args() {
-w)
SYNC_WALLPAPER=0
;;
-d)
SYNC_ALINA_DROP=0
;;
-s)
SYNC_SYNCFOLDER=0
;;
@@ -139,7 +132,6 @@ parse_args() {
SYNC_RECIPIES=0
SYNC_ALINA=0
SYNC_MEMES=0
SYNC_ALINA_DROP=0
SYNC_WALLPAPER=0
SYNC_PICTURES=0
;;
@@ -172,7 +164,6 @@ validate_sync_options() {
SYNC_ALINA == 1 &&
SYNC_MEMES == 1 &&
SYNC_SYNCFOLDER == 1 &&
SYNC_ALINA_DROP == 1 &&
SYNC_WALLPAPER == 1 &&
SYNC_PICTURES == 1 )); then
@@ -202,14 +193,6 @@ sync_all() {
"/data/nextcloud/syncuser/Syncfolder"
(( ret += $? ))
fi
if (( SYNC_ALINA_DROP == 0 )); then
sync_directory \
"Alina Share" \
"Alina Share/" \
"/data/nextcloud/syncuser/alina"
(( ret += $? ))
fi
if (( SYNC_MEMES == 0 )); then
sync_directory \
"Memes" \
+6 -54
View File
@@ -1,29 +1,11 @@
HOST=$(hostname)
if [ "${HOST}" == "quiethost" ]; then
DOC_DIR="/home/lucas/Documents"
UNSYNCED="SW"
elif [ "${HOST}" == "vivohost" ];then
UNSYNCED="Music\nSW\nPictures\nDokumente/Studium\nDokumente/Auslandsdienst"
DOC_DIR="/home/lucas/Documents"
rasp_host="someoneslinux.ddns.net"
elif [ "${HOST}" != "raspberry" ]; then
UNSYNCED="Music\nSW\nPictures "
DOC_DIR="/home/lucas/Documents"
fi
if [ "${HOST}" == "raspberry" ]; then
LOCAL_SCRIPTS="/home/lucas/Raspberry/scripts"
DOC_DIR="/home/lucas/Documents"
else
LOCAL_SCRIPTS="/home/lucas/scripts"
rasp_host=raspberry
fi
NC_SCRIPTS="$DOC_DIR/Dokumente/Projekte/Server/scripts"
LOCAL_SCRIPTS="/home/lucas/scripts"
alias please="sudo"
alias NC_scripts="cd ${NC_SCRIPTS}"
alias regex_rename="${LOCAL_SCRIPTS}/rename.sh"
alias profile="${LOCAL_SCRIPTS}/profile_motd.sh"
alias auto_update="sudo ${LOCAL_SCRIPTS}/utils/upgrade.sh"
alias auto_update="${LOCAL_SCRIPTS}/utils/upgrade.sh"
alias aptgrep="apt list --installed | grep -i "
alias reboot="sudo /sbin/reboot"
alias shutdown="sudo /sbin/shutdown"
@@ -35,6 +17,10 @@ alias pubip="wget -T 5 -qO- ifconfig.co"
alias pubip_country="wget -T 5 -qO- ifconfig.co/country"
alias ipinfo='${LOCAL_SCRIPTS}/ip_info.sh'
alias pygen="/home/lucas/scripts/.venv/bin/python3"
alias mediawake="bash ${LOCAL_SCRIPTS}/network/wol.sh mediahost -c"
alias serverwake="bash ${LOCAL_SCRIPTS}/network/wol.sh serverhost -c"
alias quietwake="bash ${LOCAL_SCRIPTS}/network/wol.sh quiethost"
if [[ "${HOST}" == "quiethost" ]] || [[ "${HOST}" == "serverhost" ]]; then
### Veracrypt aliases
#Passwd Mount and Grep
@@ -52,40 +38,6 @@ if [[ "${HOST}" == "quiethost" ]] || [[ "${HOST}" == "serverhost" ]]; then
alias recipeAI="~/.venv_general/bin/python3 ~/scripts/quiethost/generateNutritionInfowithGemini.py /data/Nextcloud/Documents/Dokumente/Shared/Diverses/Kochbuch/RecepiesLucas/"
alias ffmepeg_handling="~/.venv_general/bin/python3 ~/scripts/ffmpeg_handling.py"
fi
if [ "${HOST}" != "raspberry" ]; then
# Shutdown
alias alldown='ssh ${rasp_host} hostdown all -c'
alias serverdown='ssh ${rasp_host} hostdown serverhost -c'
alias delldown='ssh ${rasp_host} hostdown quiethost -c'
# Wake
alias serverwake="ssh ${rasp_host} wake serverhost"
alias quietwake="bash ${LOCAL_SCRIPTS}/network/wol.sh quiethost"
# SSH
alias rasp="ssh ${rasp_host}"
alias raspcopy='bash ${LOCAL_SCRIPTS}/copy.sh -h ${rasp_host} -f '
#NW Stat
alias NWscan="ssh ${rasp_host} hoststatus netw -c"
alias hoststat="ssh ${rasp_host} hoststatus hosts -c"
elif [ "${HOST}" != "vivohost" ];then
# Raspberry Shutdown
alias serverdown='hostdown serverhost -c'
alias delldown='hostdown quiethost -c'
alias alldown='hostdown all -c'
alias pcdown='hostdown pc -c'
# Raspberry Wake
alias serverwake='wake serverhost'
alias quietwake='wake quiethost'
# Raspberry Hoststatus
alias hoststat="hoststatus hosts -c"
alias NWscan="hoststatus netw -c"
# Raspberry Network Settings
alias IFchange="sudo bash ${LOCAL_SCRIPTS}/IFchange.sh"
alias IPsettings="sudo bash /home/lucas/Raspberry/scripts/iptables.sh"
alias IPtables-save='sudo sh -c '\''iptables-save > /etc/network/iptables'\'''
# Raspberry Owncloud
alias syncNC="nextcloudcmd --path Dokumente/Dokumente/Projekte/Server /home/lucas/Documents/Dokumente/Projekte/Server https://lucas:S1mpleRe1e4sedP1ayingDesign@someoneslinux.ddns.net"
fi
if [ "${HOST}" != "serverhost" ]; then
alias serverhost="ssh -X serverhost"
alias lstorr="ssh serverhost du -a -d1 -h /torrent/complete/ | sed 's/\/torrent\/complete\///'g | head -n -1"
+15
View File
@@ -0,0 +1,15 @@
# /etc/exports: the access control list for filesystems which may be exported
# to NFS clients. See exports(5).
#
# Example for NFSv2 and NFSv3:
# /srv/homes hostname1(rw,sync,no_subtree_check) hostname2(ro,sync,no_subtree_check)
#
# Example for NFSv4:
# /srv/nfs4 gss/krb5i(rw,sync,fsid=0,crossmnt,no_subtree_check)
# /srv/nfs4/homes gss/krb5i(rw,sync,no_subtree_check)
#
# sudo apt install nfs-kernel-server
# sudo exportfs -ra
/torrent/qbittorrent/downloads 192.168.0.4(rw,sync,no_subtree_check)
/backup/homeassistant 192.168.0.4(rw,sync,no_subtree_check)
+5
View File
@@ -0,0 +1,5 @@
192.168.0.2 serverhost
192.168.0.39 quiethost
192.168.0.4 mediahost
192.168.0.81 vivohost
#192.168.0.122 homeassistant
+84
View File
@@ -0,0 +1,84 @@
## /etc/smartd.conf
# ============================================================================
# Common options:
#
# -a Enable all standard SMART monitoring:
# * SMART health status
# * Error logs
# * Self-test logs
# * Attribute monitoring
#
# -o on Enable automatic offline data collection
#
# -S on Enable SMART attribute autosave
#
# -s (...) Schedule SMART self-tests
#
# Test schedule syntax:
# S = Short self-test
# L = Long/Extended self-test
#
# Fields:
# (TYPE/MM/DD/DW/HH)
#
# MM = month (01-12 or .. for every month)
# DD = day of month (01-31 or .. for every day)
# DW = day of week (1-7, where 7=Sunday)
# HH = hour (00-23)
#
# Schedule used below:
# S/../../7/02
# → Run SHORT test every Sunday at 02:00
#
# L/../01/./03
# → Run LONG test on the 1st day of every month at 03:00
#
# ============================================================================
# SERVERHOST
# ============================================================================
# # WD Red 3 TB Backup Drive
#/dev/sda -a -o on -S on -s (S/../../7/02|L/../01/./03)
# Samsung/Seagate 2 TB Laptop Drive
# /dev/sdb -a -o on -S on -s (S/../../7/02|L/../01/./03)
# ============================================================================
# MEDIAHOST
# ============================================================================
#
## WD Gold 16 TB
#/dev/sda -a -o on -S on -s (S/../../7/02|L/../01/./03)
#
## WD Red Pro 16 TB
#/dev/sdb -a -o on -S on -s (S/../../7/02|L/../01/./03)
#
## WD Red Pro 16 TB
#/dev/sdc -a -o on -S on -s (S/../../7/02|L/../01/./03)
#
## Seagate Exos 8 TB
#/dev/sdd -a -o on -S on -s (S/../../7/02|L/../01/./03)
#
## WD Blue SN5100 NVMe
## NVMe drives support SMART differently, so -o and -S are not needed.
#/dev/nvme0n1 -a -s (S/../../7/02|L/../01/./03)
#
# ============================================================================
# QUIETHOST
# ============================================================================
#
## Seagate IronWolf 8TB
## /dev/sda
#/dev/sda -a -o on -S on -s (S/../../7/02|L/../01/./03)
#
## Intenso 250GB NVMe
## /dev/nvme0n1
## NVMe devices do not use ATA offline data collection,
## therefore -o on and -S on are omitted.
#/dev/nvme0n1 -a -s (S/../../7/02|L/../01/./03)
#
## Crucial P1 1TB NVMe
## /dev/nvme1n1
#/dev/nvme1n1 -a -s (S/../../7/02|L/../01/./03)#
#
+15
View File
@@ -0,0 +1,15 @@
Host serverhost
HostName someoneslinux.ddns.net
User lucas
Host raspberry
HostName raspberry
User lucas
Host quiethost
HostName quiethost
User lucas
Host mediahost
HostName mediahost
User lucas
+12
View File
@@ -0,0 +1,12 @@
[Unit]
Description=Prometheus Smartctl Exporter
After=network.target
[Service]
User=root
ExecStart=/opt/smartctl_exporter/smartctl_exporter
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
-46
View File
@@ -295,52 +295,6 @@ def rename_media(media_data,episodes, series_path,file_title):
print("Cancelled.")
return
def process_series(series_path: Path):
series = series_path.name
m = re.match(r"^(.*?)\s*\((\d{4})\)\s*\[(\d+)\]$", series)
if not m:
print(f"Skipping invalid folder: {series}")
return
file_title = m.group(1).strip()
file_year = int(m.group(2))
file_tmdb_id = int(m.group(3))
print()
print("=" * 80)
print(f"TITLE={file_title}")
print(f"YEAR={file_year}")
print(f"TMDB_ID={file_tmdb_id}")
if file_tmdb_id == 0:
print("TMDB ID required")
return
try:
data = find_by_tmdb_id(file_tmdb_id)
if not data:
print("TMDB lookup failed")
return
media_type, media_data = data
print_results(data)
episodes = get_episode_list(media_data)
rename_media(
media_data,
episodes,
series_path,
file_title
)
except Exception as e:
print(f"Error processing {series}: {e}")
def process_series(series_path: Path):
series = series_path.name
+3 -3
View File
@@ -19,10 +19,10 @@ Exit Codes:
import sys
import paho.mqtt.client as mqtt
MQTT_BROKER_HOST = "192.168.0.122"
MQTT_BROKER_HOST = "192.168.0.4"
MQTT_BROKER_PORT = 1883
MQTT_USERNAME = "MQTTBrokerUser"
MQTT_PASSWORD = "mqtt1510##"
MQTT_USERNAME = "mqttbrokeruser"
MQTT_PASSWORD = "TKtWrtWGLSouf6"
MQTT_TOPIC = "serverhost_torrent_sensor/topic"
+40 -62
View File
@@ -55,7 +55,6 @@ EOF
}
home_dir="/home/lucas"
venv_dir="${home_dir}/tools/spotdl_venv"
green="\e[32m"
red="\e[31m"
@@ -161,7 +160,7 @@ for ((i=0; i<${#CMD[@]}; i++)); do
printf "Running: %s\n" "${CMD[$i]}"
if eval "${CMD[$i]}"; then
if eval "sudo ${CMD[$i]}"; then
write_out "${MSG[$i]} [${green}OK${norm}]"
else
write_out "${MSG[$i]} [${red}FAIL${norm}]"
@@ -177,7 +176,7 @@ if command -v flatpak >/dev/null 2>&1; then
echo "Starting Flatpak Update..."
flatpak update -y --noninteractive
sudo flatpak update -y --noninteractive
ret=$?
@@ -196,69 +195,44 @@ echo "Finished Package Update"
# SpotDL Virtual Environment
###############################################################################
if [[ ! -d "$venv_dir" ]] && [[ "$1" == "--create-venv" ]]; then
echo "Creating SpotDL virtual environment"
PACKAGES=(
yt-dlp
spotdl
)
mkdir -p "${home_dir}/tools"
python3 -m venv "$venv_dir"
# Install pipx if missing
if ! command -v pipx >/dev/null 2>&1; then
echo "Installing pipx..."
sudo apt update
sudo apt install -y pipx
pipx ensurepath
fi
if [[ -d "$venv_dir" ]]; then
if [[ ! -x "${venv_dir}/bin/pip3" ]]; then
echo "Installing pip"
"${venv_dir}/bin/python" -m ensurepip --upgrade
# Check ffmpeg
if ! command -v ffmpeg >/dev/null 2>&1; then
echo "Installing ffmpeg..."
sudo apt install -y ffmpeg
fi
# Install or update packages
for pkg in "${PACKAGES[@]}"; do
if pipx list --short 2>/dev/null | grep -qx "$pkg"; then
echo "Updating $pkg..."
pipx upgrade "$pkg"
else
echo "Installing $pkg..."
pipx install "$pkg"
fi
echo "Updating venv tooling"
"${venv_dir}/bin/pip3" install --upgrade \
pip \
setuptools \
wheel
###############################################################################
# Ensure required packages exist
###############################################################################
mkdir -p "${home_dir}/.local/bin"
packages=(
spotdl
yt-dlp
youtube-dl
)
for package in "${packages[@]}"; do
echo "Checking package: ${package}"
if ! "${venv_dir}/bin/pip3" show "$package" >/dev/null 2>&1; then
echo "Installing ${package}"
"${venv_dir}/bin/pip3" install "$package"
else
echo "Updating ${package}"
"${venv_dir}/bin/pip3" install --upgrade "$package"
fi
if [[ -x "${venv_dir}/bin/${bin}" ]]; then
ln -sf "${venv_dir}/bin/${package}" "${home_dir}/.local/bin/${package}"
fi
if [[ $? -eq 0 ]]; then
write_out "${package} [${green}OK${norm}]"
else
write_out "${package} [${red}FAIL${norm}]"
fi
done
done
# Install Deno for spotdl if missing
if ! command -v deno >/dev/null 2>&1; then
echo "Installing Deno..."
curl -fsSL https://deno.land/install.sh | sh
# Add to PATH for current shell
export PATH="$HOME/.deno/bin:$PATH"
fi
###############################################################################
@@ -294,6 +268,10 @@ if command -v docker >/dev/null 2>&1; then
fi
)
;;
nextcloud:*)
docker pull "$image"
docker exec -it nextcloud php occ app:update --all
;;
*"<none>"*)
;;
*)