Compare commits
16
Commits
7158dd4960
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b0b438740 | ||
|
|
2eda7cd33e | ||
|
|
efc150d9ce | ||
|
|
044fbd81ad | ||
|
|
01ab319eea | ||
|
|
d5e627325d | ||
|
|
deaff35bc3 | ||
|
|
27d570fc5c | ||
|
|
d5fb01adb2 | ||
|
|
52c02beb3d | ||
|
|
3f14f53184 | ||
|
|
c41bf36afb | ||
|
|
07bab58ae0 | ||
|
|
d06cff4cbd | ||
|
|
09343a7523 | ||
|
|
dc0cd02e58 |
No files matched your search
@@ -8,7 +8,6 @@ import subprocess
|
|||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import time
|
import time
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
RED = "\033[91m"
|
RED = "\033[91m"
|
||||||
GREEN = "\033[92m"
|
GREEN = "\033[92m"
|
||||||
@@ -145,36 +144,6 @@ def create_m4b(root_path: Path, book_dir: Path) -> None:
|
|||||||
else:
|
else:
|
||||||
log_message(f"{RED}[ERROR]{RESET} - {book_dir.name} - 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) -> tuple[list[Any], int]:
|
|
||||||
"""
|
|
||||||
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"))
|
|
||||||
+ list(root.rglob("*.opus"))
|
|
||||||
)
|
|
||||||
|
|
||||||
counts: dict[Path, int] = {}
|
|
||||||
|
|
||||||
for file in media_files:
|
|
||||||
if counts.get(file.parent, 0) > 1:
|
|
||||||
continue
|
|
||||||
counts[file.parent] = counts.get(file.parent, 0) + 1
|
|
||||||
|
|
||||||
directories = []
|
|
||||||
size = 0
|
|
||||||
for directory, count in counts.items():
|
|
||||||
if count > 1:
|
|
||||||
directories.append(directory)
|
|
||||||
size += sum(file.stat().st_size for pattern in ("*.mp3", "*.m4a", "*.m4b", "*.opus") for file in directory.glob(pattern))
|
|
||||||
|
|
||||||
|
|
||||||
return sorted(directories), size
|
|
||||||
|
|
||||||
def process_book(args):
|
def process_book(args):
|
||||||
target, book_dir = args
|
target, book_dir = args
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
@@ -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"
|
list="$1"
|
||||||
|
|
||||||
while IFS= read -r file; do
|
while IFS= read -r file; do
|
||||||
|
book
|
||||||
[[ -z "$file" ]] && continue
|
[[ -z "$file" ]] && continue
|
||||||
|
|
||||||
if [[ ! -e "$file" ]]; then
|
if [[ ! -e "$file" ]]; then
|
||||||
|
|||||||
@@ -16,4 +16,10 @@ services:
|
|||||||
- /data/gitea:/data
|
- /data/gitea:/data
|
||||||
- /etc/timezone:/etc/timezone:ro
|
- /etc/timezone:/etc/timezone:ro
|
||||||
- /etc/localtime:/etc/localtime: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
|
TZ: Europe/Vienna
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/homeassistant:/config
|
- /srv/homeassistant:/config
|
||||||
|
devices:
|
||||||
|
- /dev/bus/usb:/dev/bus/usb
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
- NET_RAW
|
||||||
|
|
||||||
mosquitto:
|
mosquitto:
|
||||||
container_name: 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:
|
networks:
|
||||||
- default
|
- default
|
||||||
- shared-proxy-services
|
- shared-proxy-services
|
||||||
|
|
||||||
seerr:
|
seerr:
|
||||||
image: ghcr.io/seerr-team/seerr:latest
|
image: ghcr.io/seerr-team/seerr:latest
|
||||||
container_name: seerr
|
container_name: seerr
|
||||||
@@ -29,6 +28,7 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- default
|
- default
|
||||||
- shared-proxy-services
|
- shared-proxy-services
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
external: true
|
external: true
|
||||||
|
|||||||
@@ -1,22 +1,5 @@
|
|||||||
|
|
||||||
services:
|
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:
|
sonarr-de:
|
||||||
image: lscr.io/linuxserver/sonarr:latest
|
image: lscr.io/linuxserver/sonarr:latest
|
||||||
container_name: sonarr-de
|
container_name: sonarr-de
|
||||||
@@ -31,10 +14,13 @@ services:
|
|||||||
- ./sonarr_DE:/config
|
- ./sonarr_DE:/config
|
||||||
- /data/jellyfin/media/series-german:/JellyfinSeriesGerman
|
- /data/jellyfin/media/series-german:/JellyfinSeriesGerman
|
||||||
- /data/jellyfin/media/anime-german:/JellyfinAnimeGerman
|
- /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/media/series-kids:/JellyfinKinder
|
||||||
- /data/jellyfin/buffer:/buffer
|
- /data/jellyfin/buffer:/buffer
|
||||||
- /torrent/qbittorrent/downloads:/downloads
|
- /torrent/qbittorrent/downloads:/downloads
|
||||||
- /torrent/rundfunkarr:/downloads-rundfunkarr
|
- /torrent/rundfunkarr-de:/downloads-rundfunkarr
|
||||||
radarr-en:
|
radarr-en:
|
||||||
image: lscr.io/linuxserver/radarr:latest
|
image: lscr.io/linuxserver/radarr:latest
|
||||||
container_name: radarr-en
|
container_name: radarr-en
|
||||||
@@ -67,7 +53,7 @@ services:
|
|||||||
- /data/jellyfin/media/movies-kids:/JellyfinKinder
|
- /data/jellyfin/media/movies-kids:/JellyfinKinder
|
||||||
- /data/jellyfin/buffer:/buffer
|
- /data/jellyfin/buffer:/buffer
|
||||||
- /torrent/qbittorrent/downloads:/downloads
|
- /torrent/qbittorrent/downloads:/downloads
|
||||||
- /torrent/rundfunkarr:/downloads-rundfunkarr
|
- /torrent/rundfunkarr-de:/downloads-rundfunkarr
|
||||||
prowlarr:
|
prowlarr:
|
||||||
image: lscr.io/linuxserver/prowlarr:latest
|
image: lscr.io/linuxserver/prowlarr:latest
|
||||||
container_name: prowlarr
|
container_name: prowlarr
|
||||||
@@ -177,7 +163,7 @@ services:
|
|||||||
- TZ=Europe/Berlin
|
- TZ=Europe/Berlin
|
||||||
volumes:
|
volumes:
|
||||||
- ./lidarr:/config
|
- ./lidarr:/config
|
||||||
- /data/navidrome/music:/music
|
- /data/navidrome/music/music:/music
|
||||||
- /torrent/qbittorrent/downloads:/downloads
|
- /torrent/qbittorrent/downloads:/downloads
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
@@ -261,14 +247,43 @@ services:
|
|||||||
- /data/jellyfin/media/comedy-english:/data/comedy-english:ro
|
- /data/jellyfin/media/comedy-english:/data/comedy-english:ro
|
||||||
- /data/jellyfin/media/movies-german:/data/movies-german:ro
|
- /data/jellyfin/media/movies-german:/data/movies-german:ro
|
||||||
- /data/jellyfin/media/movies-kids:/data/movies-kids: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/navidrome/music:/data/music:ro
|
||||||
- /data/audiobookshelf/data/audiobooks-englisch:/data/audiobooks-english:ro
|
- /data/audiobookshelf/data/audiobooks-englisch:/data/audiobooks-english:ro
|
||||||
- /data/audiobookshelf/data/audiobooks-deutsch:/data/audiobooks-german:ro
|
- /data/audiobookshelf/data/audiobooks-deutsch:/data/audiobooks-german:ro
|
||||||
- /data/audiobookshelf/data/audiobooks-kinder:/data/audiobooks-kinder:ro
|
- /data/audiobookshelf/data/audiobooks-kinder:/data/audiobooks-kinder:ro
|
||||||
# Download location
|
# Download location
|
||||||
- /torrent/qbittorrent/downloads:/downloads
|
- /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:
|
networks:
|
||||||
default:
|
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:
|
volumes:
|
||||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||||
- prometheus-data:/prometheus
|
- prometheus-data:/prometheus
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- shared-proxy-services
|
||||||
|
|
||||||
grafana:
|
grafana:
|
||||||
image: grafana/grafana:latest
|
image: grafana/grafana:latest
|
||||||
@@ -47,9 +50,9 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- '/:/host:ro,rslave'
|
- '/:/host:ro,rslave'
|
||||||
- ./node_exporter/textfile_collector:/textfile:ro
|
- ./node_exporter/textfile_collector:/textfile:ro
|
||||||
ports:
|
# ports:
|
||||||
- '9100:9100'
|
# - '9100:9100'
|
||||||
|
network_mode: host
|
||||||
cadvisor:
|
cadvisor:
|
||||||
image: gcr.io/cadvisor/cadvisor:latest
|
image: gcr.io/cadvisor/cadvisor:latest
|
||||||
container_name: cadvisor
|
container_name: cadvisor
|
||||||
@@ -58,9 +61,24 @@ services:
|
|||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- /:/rootfs:ro
|
- /:/rootfs:ro
|
||||||
- /data:/data:ro
|
# - /data:/data:ro
|
||||||
- /docker:/docker: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:
|
volumes:
|
||||||
prometheus-data:
|
prometheus-data:
|
||||||
grafana-data:
|
grafana-data:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
global:
|
global:
|
||||||
scrape_interval: 15s
|
scrape_interval: 30s
|
||||||
evaluation_interval: 15s
|
evaluation_interval: 30s
|
||||||
|
|
||||||
scrape_configs:
|
scrape_configs:
|
||||||
|
|
||||||
@@ -8,6 +8,35 @@ scrape_configs:
|
|||||||
static_configs:
|
static_configs:
|
||||||
- targets:
|
- targets:
|
||||||
- "prometheus:9090"
|
- "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"
|
- job_name: "node-exporter"
|
||||||
static_configs:
|
static_configs:
|
||||||
@@ -16,20 +45,33 @@ scrape_configs:
|
|||||||
labels:
|
labels:
|
||||||
host: serverhost
|
host: serverhost
|
||||||
- targets:
|
- targets:
|
||||||
- "node-exporter:9100"
|
- "192.168.0.4:9100"
|
||||||
labels:
|
labels:
|
||||||
host: "mediahost"
|
host: "mediahost"
|
||||||
|
- targets:
|
||||||
|
- "192.168.0.39:9100"
|
||||||
|
labels:
|
||||||
|
host: "quiethost"
|
||||||
- job_name: "cadvisor"
|
- job_name: "cadvisor"
|
||||||
static_configs:
|
static_configs:
|
||||||
- targets:
|
- targets:
|
||||||
- "cadvisor:8080"
|
- "cadvisor:8080"
|
||||||
|
labels:
|
||||||
|
host: mediahost
|
||||||
|
- targets:
|
||||||
|
- "192.168.0.2:8081"
|
||||||
|
labels:
|
||||||
|
host: serverhost
|
||||||
- job_name: "smartctl"
|
- job_name: "smartctl"
|
||||||
static_configs:
|
static_configs:
|
||||||
- targets:
|
- targets:
|
||||||
- "192.168.0.2:9633"
|
- "192.168.0.2:9633"
|
||||||
labels:
|
labels:
|
||||||
host: serverhost
|
host: serverhost
|
||||||
|
- targets:
|
||||||
|
- "192.168.0.39:9633"
|
||||||
|
labels:
|
||||||
|
host: "quiethost"
|
||||||
- targets:
|
- targets:
|
||||||
- "192.168.0.4:9633"
|
- "192.168.0.4:9633"
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ services:
|
|||||||
- /data/navidrome/music/kamerun:/kamerun:ro
|
- /data/navidrome/music/kamerun:/kamerun:ro
|
||||||
- /data/navidrome/music/weihnachten:/weihnachten:ro
|
- /data/navidrome/music/weihnachten:/weihnachten:ro
|
||||||
- /data/navidrome/music/playlists:/playlists:ro
|
- /data/navidrome/music/playlists:/playlists:ro
|
||||||
|
- /data/navidrome/music/playlists-export:/playlists-export
|
||||||
networks:
|
networks:
|
||||||
- shared-proxy-services
|
- shared-proxy-services
|
||||||
- default
|
- default
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ server {
|
|||||||
passwd.someoneslinux.selfhost.eu
|
passwd.someoneslinux.selfhost.eu
|
||||||
requests.someoneslinux.selfhost.eu
|
requests.someoneslinux.selfhost.eu
|
||||||
torrent.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:secp256r1:secp384r1;
|
||||||
# ssl_conf_command Groups X25519:prime256v1:secp384r1;
|
# ssl_conf_command Groups X25519:prime256v1:secp384r1;
|
||||||
location /.well-known/acme-challenge/ {
|
location /.well-known/acme-challenge/ {
|
||||||
@@ -27,12 +28,28 @@ server {
|
|||||||
|
|
||||||
location / {
|
location / {
|
||||||
return 301 https://$host$request_uri;
|
return 301 https://$host$request_uri;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
listen 8080;
|
||||||
|
location /nginx_status {
|
||||||
|
stub_status;
|
||||||
|
allow 172.16.0.0/12;
|
||||||
|
deny all;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Common SSL settings
|
# Common SSL settings
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_prefer_server_ciphers on;
|
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
|
# NEXTCLOUD
|
||||||
@@ -51,6 +68,8 @@ server {
|
|||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://nextcloud:80;
|
proxy_pass http://nextcloud:80;
|
||||||
|
# Do not try another upstream on errors
|
||||||
|
proxy_next_upstream off;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Forwarded-Proto https;
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
@@ -76,6 +95,8 @@ server {
|
|||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://jellyfin:8096;
|
proxy_pass http://jellyfin:8096;
|
||||||
|
# Do not try another upstream on errors
|
||||||
|
proxy_next_upstream off;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
@@ -103,6 +124,8 @@ server {
|
|||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://audiobookshelf:80;
|
proxy_pass http://audiobookshelf:80;
|
||||||
|
# Do not try another upstream on errors
|
||||||
|
proxy_next_upstream off;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
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 {
|
#server {
|
||||||
# listen 443 ssl;http2 on;
|
# 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-Frame-Options "SAMEORIGIN" always;
|
||||||
# add_header X-Content-Type-Options "nosniff" always;
|
# add_header X-Content-Type-Options "nosniff" always;
|
||||||
# add_header Referrer-Policy "strict-origin-when-cross-origin" 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;
|
# ssl_certificate_key /etc/nginx/certs/live/cloud.someoneslinux.selfhost.eu/privkey.pem;
|
||||||
#
|
#
|
||||||
# location / {
|
# location / {
|
||||||
# proxy_pass http://192.168.0.4:8123;
|
# proxy_pass http://authentik-server-1:9000;
|
||||||
# proxy_http_version 1.1;
|
# 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-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
|
# NAVIDROME
|
||||||
###################
|
###################
|
||||||
@@ -184,6 +209,8 @@ server {
|
|||||||
location / {
|
location / {
|
||||||
proxy_pass http://navidrome:4533;
|
proxy_pass http://navidrome:4533;
|
||||||
|
|
||||||
|
# Do not try another upstream on errors
|
||||||
|
proxy_next_upstream off;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Forwarded-Proto https;
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
@@ -208,6 +235,8 @@ server {
|
|||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://vaultwarden:80;
|
proxy_pass http://vaultwarden:80;
|
||||||
|
# Do not try another upstream on errors
|
||||||
|
proxy_next_upstream off;
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Forwarded-Proto https;
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
@@ -216,6 +245,8 @@ server {
|
|||||||
|
|
||||||
location /notifications/hub {
|
location /notifications/hub {
|
||||||
proxy_pass http://vaultwarden:3012;
|
proxy_pass http://vaultwarden:3012;
|
||||||
|
# Do not try another upstream on errors
|
||||||
|
proxy_next_upstream off;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
@@ -240,6 +271,8 @@ server {
|
|||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://seerr:5055;
|
proxy_pass http://seerr:5055;
|
||||||
|
# Do not try another upstream on errors
|
||||||
|
proxy_next_upstream off;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
@@ -268,6 +301,8 @@ server {
|
|||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://192.168.0.2:8080;
|
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_http_version 1.1;
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
@@ -275,6 +310,7 @@ server {
|
|||||||
proxy_set_header Connection $connection_upgrade;
|
proxy_set_header Connection $connection_upgrade;
|
||||||
proxy_set_header X-Forwarded-Proto https;
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
###################
|
###################
|
||||||
@@ -294,6 +330,8 @@ server {
|
|||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://grafana:3000;
|
proxy_pass http://grafana:3000;
|
||||||
|
# Do not try another upstream on errors
|
||||||
|
proxy_next_upstream off;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
@@ -302,3 +340,33 @@ server {
|
|||||||
proxy_set_header X-Forwarded-Proto https;
|
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
|
image: nginx:stable
|
||||||
container_name: nginx
|
container_name: nginx
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./conf.d:/etc/nginx/conf.d
|
- ./conf.d:/etc/nginx/conf.d
|
||||||
- ./certs:/etc/nginx/certs
|
- ./certs:/etc/nginx/certs
|
||||||
@@ -15,6 +13,21 @@ services:
|
|||||||
# Access and error logs
|
# Access and error logs
|
||||||
- ./logs:/var/log/nginx
|
- ./logs:/var/log/nginx
|
||||||
networks:
|
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
|
- shared-proxy-services
|
||||||
|
|
||||||
crowdsec:
|
crowdsec:
|
||||||
@@ -29,7 +42,8 @@ services:
|
|||||||
- crowdsec-db:/var/lib/crowdsec/data
|
- crowdsec-db:/var/lib/crowdsec/data
|
||||||
- ./logs:/var/log/nginx:ro
|
- ./logs:/var/log/nginx:ro
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
|
||||||
certbot:
|
certbot:
|
||||||
image: certbot/certbot:latest
|
image: certbot/certbot:latest
|
||||||
@@ -41,7 +55,9 @@ services:
|
|||||||
- shared-proxy-services
|
- shared-proxy-services
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
|
default:
|
||||||
shared-proxy-services:
|
shared-proxy-services:
|
||||||
external: true
|
external: true
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
crowdsec-db:
|
crowdsec-db:
|
||||||
@@ -3,7 +3,7 @@ services:
|
|||||||
image: redis:7
|
image: redis:7
|
||||||
container_name: redis
|
container_name: redis
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: redis-server --appendonly yes --requirepass PerCent7heStorySp0tInOrd3r
|
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
|
||||||
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test:
|
||||||
|
|||||||
@@ -10,6 +10,19 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- '/:/host:ro,rslave'
|
- '/:/host:ro,rslave'
|
||||||
- ./node_exporter/textfile_collector:/textfile:ro
|
- ./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:
|
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"
|
TORRENT_NAME="$1"
|
||||||
ROOT_PATH="$2"
|
ROOT_PATH="$2"
|
||||||
CATEGORY="$3"
|
CATEGORY="$3"
|
||||||
|
if [[ "$CATEGORY" != *arr* && "$CATEGORY" != "mytorrents" ]]; then
|
||||||
|
|
||||||
if [[ "$CATEGORY" != *arr* ]]; then
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Start
|
# Start
|
||||||
###############################################################################
|
###############################################################################
|
||||||
@@ -155,7 +155,7 @@ if [[ "$CATEGORY" != *arr* ]]; then
|
|||||||
lucas@mediahost "mkdir -p \"/torrent/qbittorrent/mediahost/$remote_dir\""
|
lucas@mediahost "mkdir -p \"/torrent/qbittorrent/mediahost/$remote_dir\""
|
||||||
SSH_STATUS=$?
|
SSH_STATUS=$?
|
||||||
scp -i /config/.ssh/id_ed25519 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
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=$?
|
SCP_STATUS=$?
|
||||||
|
|
||||||
echo "scp exit status: $SCP_STATUS"
|
echo "scp exit status: $SCP_STATUS"
|
||||||
|
|||||||
Executable
+170
@@ -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()
|
||||||
@@ -167,19 +167,22 @@ process_file() {
|
|||||||
|
|
||||||
|
|
||||||
HOSTNAME_EXPECTED="mediahost"
|
HOSTNAME_EXPECTED="mediahost"
|
||||||
SOURCE_DIR="/data/nextcloud/syncuser/alina/Dropfolder"
|
SOURCE_DIR="/data/nextcloud/syncuser/pictures/Alina Share/Dropfolder"
|
||||||
TARGET_DIR="/data/nextcloud/syncuser/camera"
|
TARGET_DIR="/data/nextcloud/syncuser/pictures/Camera"
|
||||||
|
|
||||||
# If running on serverhost, perform initial sync
|
# If running on serverhost, perform initial sync
|
||||||
if [[ "$(hostname)" == "$HOSTNAME_EXPECTED" ]]; then
|
if [[ "$(hostname)" == "$HOSTNAME_EXPECTED" ]]; then
|
||||||
syncNC -d -c
|
echo Sync Nextcloud
|
||||||
|
syncNC -c
|
||||||
cd "$SOURCE_DIR" || exit 1
|
cd "$SOURCE_DIR" || exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
if [[ $# -gt 0 ]]; then
|
if [[ $# -gt 0 ]]; then
|
||||||
|
echo Process single file $1
|
||||||
process_file "$1"
|
process_file "$1"
|
||||||
else
|
else
|
||||||
|
echo Process dir .
|
||||||
shopt -s nullglob
|
shopt -s nullglob
|
||||||
files=( *.heic *.mov )
|
files=( *.heic *.mov )
|
||||||
|
|
||||||
@@ -202,7 +205,7 @@ if [[ "$(hostname)" == "$HOSTNAME_EXPECTED" ]]; then
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
shopt -u nullglob
|
shopt -u nullglob
|
||||||
|
echo Sync Nextcloud
|
||||||
syncNC -d -c
|
syncNC -c
|
||||||
fi
|
fi
|
||||||
|
|
||||||
Executable
+31
@@ -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
|
||||||
@@ -2,7 +2,13 @@
|
|||||||
|
|
||||||
OUT=/docker/monitoring/node_exporter/textfile_collector/custom.prom
|
OUT=/docker/monitoring/node_exporter/textfile_collector/custom.prom
|
||||||
TS=$(date +%s%3N)
|
TS=$(date +%s%3N)
|
||||||
|
if [[ "$(hostname)" == "mediahost" ]]; then
|
||||||
DOCKER_CONTAINER_NUM=44
|
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 container states
|
||||||
docker ps -a --format '{{.Names}}' | while read -r name; do
|
docker ps -a --format '{{.Names}}' | while read -r name; do
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
device=$1
|
device=$1
|
||||||
if [ "$device" == "serverhost" ]; then
|
if [ "$device" == "serverhost" ]; then
|
||||||
mac="64:51:06:51:cb:39"
|
mac="64:51:06:51:cb:39"
|
||||||
|
elif [ "$device" == "mediahost" ]; then
|
||||||
|
mac="9c:6b:00:a3:36:c7"
|
||||||
elif [ "$device" == "quiethost" ]; then
|
elif [ "$device" == "quiethost" ]; then
|
||||||
mac="24:4B:FE:DE:93:3E"
|
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
|
elif [ "$(echo ${device} | grep -E -o "([0-9A-Fa-f]{2}[\:]){5}[0-9A-Fa-f]{2}")" != "" ]; then
|
||||||
|
|||||||
+1
-18
@@ -25,7 +25,6 @@ SYNC_MEMES=1
|
|||||||
SYNC_AUDIOBOOKS=1
|
SYNC_AUDIOBOOKS=1
|
||||||
SYNC_RECIPIES=1
|
SYNC_RECIPIES=1
|
||||||
SYNC_ALINA=1
|
SYNC_ALINA=1
|
||||||
SYNC_ALINA_DROP=1
|
|
||||||
SYNC_WALLPAPER=1
|
SYNC_WALLPAPER=1
|
||||||
SYNC_PICTURES=1
|
SYNC_PICTURES=1
|
||||||
SYNC_SYNCFOLDER=1
|
SYNC_SYNCFOLDER=1
|
||||||
@@ -39,13 +38,12 @@ Synchronize configured directories with Nextcloud.
|
|||||||
Options:
|
Options:
|
||||||
-h, --help Show this help message and exit
|
-h, --help Show this help message and exit
|
||||||
-l Show synchronization output
|
-l Show synchronization output
|
||||||
-d Enable Alina Share synchronization
|
|
||||||
-c Enable Pictures synchronization
|
-c Enable Pictures synchronization
|
||||||
-w Enable Wallpaper synchronization
|
-w Enable Wallpaper synchronization
|
||||||
-m Enable Memes synchronization
|
-m Enable Memes synchronization
|
||||||
-s Enable Syncfolder synchronization
|
-s Enable Syncfolder synchronization
|
||||||
-ab Enable Audiobook synchronization
|
-ab Enable Audiobook synchronization
|
||||||
-al Enable Alina synchronization
|
-al Enable Alina Documents synchronization
|
||||||
-all Enable all synchronization operations
|
-all Enable all synchronization operations
|
||||||
-del Remove the Nextcloud sync marker
|
-del Remove the Nextcloud sync marker
|
||||||
--version Show script version
|
--version Show script version
|
||||||
@@ -109,11 +107,6 @@ parse_args() {
|
|||||||
-w)
|
-w)
|
||||||
SYNC_WALLPAPER=0
|
SYNC_WALLPAPER=0
|
||||||
;;
|
;;
|
||||||
|
|
||||||
-d)
|
|
||||||
SYNC_ALINA_DROP=0
|
|
||||||
;;
|
|
||||||
|
|
||||||
-s)
|
-s)
|
||||||
SYNC_SYNCFOLDER=0
|
SYNC_SYNCFOLDER=0
|
||||||
;;
|
;;
|
||||||
@@ -139,7 +132,6 @@ parse_args() {
|
|||||||
SYNC_RECIPIES=0
|
SYNC_RECIPIES=0
|
||||||
SYNC_ALINA=0
|
SYNC_ALINA=0
|
||||||
SYNC_MEMES=0
|
SYNC_MEMES=0
|
||||||
SYNC_ALINA_DROP=0
|
|
||||||
SYNC_WALLPAPER=0
|
SYNC_WALLPAPER=0
|
||||||
SYNC_PICTURES=0
|
SYNC_PICTURES=0
|
||||||
;;
|
;;
|
||||||
@@ -172,7 +164,6 @@ validate_sync_options() {
|
|||||||
SYNC_ALINA == 1 &&
|
SYNC_ALINA == 1 &&
|
||||||
SYNC_MEMES == 1 &&
|
SYNC_MEMES == 1 &&
|
||||||
SYNC_SYNCFOLDER == 1 &&
|
SYNC_SYNCFOLDER == 1 &&
|
||||||
SYNC_ALINA_DROP == 1 &&
|
|
||||||
SYNC_WALLPAPER == 1 &&
|
SYNC_WALLPAPER == 1 &&
|
||||||
SYNC_PICTURES == 1 )); then
|
SYNC_PICTURES == 1 )); then
|
||||||
|
|
||||||
@@ -202,14 +193,6 @@ sync_all() {
|
|||||||
"/data/nextcloud/syncuser/Syncfolder"
|
"/data/nextcloud/syncuser/Syncfolder"
|
||||||
(( ret += $? ))
|
(( ret += $? ))
|
||||||
fi
|
fi
|
||||||
if (( SYNC_ALINA_DROP == 0 )); then
|
|
||||||
sync_directory \
|
|
||||||
"Alina Share" \
|
|
||||||
"Alina Share/" \
|
|
||||||
"/data/nextcloud/syncuser/alina"
|
|
||||||
(( ret += $? ))
|
|
||||||
fi
|
|
||||||
|
|
||||||
if (( SYNC_MEMES == 0 )); then
|
if (( SYNC_MEMES == 0 )); then
|
||||||
sync_directory \
|
sync_directory \
|
||||||
"Memes" \
|
"Memes" \
|
||||||
|
|||||||
@@ -1,24 +1,6 @@
|
|||||||
HOST=$(hostname)
|
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"
|
LOCAL_SCRIPTS="/home/lucas/scripts"
|
||||||
rasp_host=raspberry
|
|
||||||
fi
|
|
||||||
NC_SCRIPTS="$DOC_DIR/Dokumente/Projekte/Server/scripts"
|
|
||||||
alias please="sudo"
|
alias please="sudo"
|
||||||
alias NC_scripts="cd ${NC_SCRIPTS}"
|
alias NC_scripts="cd ${NC_SCRIPTS}"
|
||||||
alias regex_rename="${LOCAL_SCRIPTS}/rename.sh"
|
alias regex_rename="${LOCAL_SCRIPTS}/rename.sh"
|
||||||
@@ -35,6 +17,10 @@ alias pubip="wget -T 5 -qO- ifconfig.co"
|
|||||||
alias pubip_country="wget -T 5 -qO- ifconfig.co/country"
|
alias pubip_country="wget -T 5 -qO- ifconfig.co/country"
|
||||||
alias ipinfo='${LOCAL_SCRIPTS}/ip_info.sh'
|
alias ipinfo='${LOCAL_SCRIPTS}/ip_info.sh'
|
||||||
alias pygen="/home/lucas/scripts/.venv/bin/python3"
|
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
|
if [[ "${HOST}" == "quiethost" ]] || [[ "${HOST}" == "serverhost" ]]; then
|
||||||
### Veracrypt aliases
|
### Veracrypt aliases
|
||||||
#Passwd Mount and Grep
|
#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 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"
|
alias ffmepeg_handling="~/.venv_general/bin/python3 ~/scripts/ffmpeg_handling.py"
|
||||||
fi
|
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
|
if [ "${HOST}" != "serverhost" ]; then
|
||||||
alias serverhost="ssh -X serverhost"
|
alias serverhost="ssh -X serverhost"
|
||||||
alias lstorr="ssh serverhost du -a -d1 -h /torrent/complete/ | sed 's/\/torrent\/complete\///'g | head -n -1"
|
alias lstorr="ssh serverhost du -a -d1 -h /torrent/complete/ | sed 's/\/torrent\/complete\///'g | head -n -1"
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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)#
|
||||||
|
#
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -268,6 +268,10 @@ if command -v docker >/dev/null 2>&1; then
|
|||||||
fi
|
fi
|
||||||
)
|
)
|
||||||
;;
|
;;
|
||||||
|
nextcloud:*)
|
||||||
|
docker pull "$image"
|
||||||
|
docker exec -it nextcloud php occ app:update --all
|
||||||
|
;;
|
||||||
*"<none>"*)
|
*"<none>"*)
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
|
|||||||
Reference in new issue
Block a user