Compare commits
158
Commits
master
..
d06cff4cbd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d06cff4cbd | ||
|
|
09343a7523 | ||
|
|
dc0cd02e58 | ||
|
|
7158dd4960 | ||
|
|
4c299c5a38 | ||
|
|
2f3a763054 | ||
|
|
f7ff789d81 | ||
|
|
7c6a121504 | ||
|
|
144d974731 | ||
|
|
9e4901699d | ||
|
|
8c29731ddc | ||
|
|
80ad918686 | ||
|
|
3d585d14e0 | ||
|
|
8b6fc7fdcd | ||
|
|
e4f1cce494 | ||
|
|
8cccf674cf | ||
|
|
86679e4e18 | ||
|
|
fc9a2dcf0e | ||
|
|
57520eaa04 | ||
|
|
f9ee0a0694 | ||
|
|
1af47022e9 | ||
|
|
ce656b6970 | ||
|
|
881bdcd925 | ||
|
|
b4898bddfe | ||
|
|
860100c4de | ||
|
|
fb46bfcd3a | ||
|
|
c68b09ffd1 | ||
|
|
2ee909e6bb | ||
|
|
1d2c57dc02 | ||
|
|
3514f3e489 | ||
|
|
5b7cb93ebe | ||
|
|
b588773026 | ||
|
|
ab80645034 | ||
|
|
24b12ec6d6 | ||
|
|
a4da995ee1 | ||
|
|
39d8fb6cc5 | ||
|
|
8feec34575 | ||
|
|
e55b6ab2f2 | ||
|
|
f65e51509a | ||
|
|
e0a7968757 | ||
|
|
77ac4654bb | ||
|
|
b4d3aa1ae5 | ||
|
|
6fe6a54e86 | ||
|
|
ec48d2ef2d | ||
|
|
0e45389c60 | ||
|
|
72fe77f040 | ||
|
|
3c69b2c915 | ||
|
|
342d0ddbf5 | ||
|
|
4ccf2ac412 | ||
|
|
84d5223a95 | ||
|
|
d29489f498 | ||
|
|
7bcddd9073 | ||
|
|
286e5fa950 | ||
|
|
d1887382f2 | ||
|
|
58baa5f011 | ||
|
|
47cfca3c13 | ||
|
|
11f53f9548 | ||
|
|
6a2fab5e9e | ||
|
|
fb7e0e8cff | ||
|
|
3c7272abe5 | ||
|
|
fd77d08ef0 | ||
|
|
d992905301 | ||
|
|
0d059bc1c0 | ||
|
|
0c0920d3df | ||
|
|
73cb3802fb | ||
|
|
f8f0fd6fad | ||
|
|
b92bdbea7d | ||
|
|
366c26f581 | ||
|
|
a1aa5984e1 | ||
|
|
db90b1ae36 | ||
|
|
0d8ae83626 | ||
|
|
3168e3928b | ||
|
|
e5629b73ae | ||
|
|
653a367d92 | ||
|
|
fcb901e64d | ||
|
|
8d9a32879b | ||
|
|
6969dae72b | ||
|
|
a844fa9609 | ||
|
|
d34cd72f7a | ||
|
|
93a0c3ca79 | ||
|
|
b1f316667e | ||
|
|
d5cdead474 | ||
|
|
34dfb4da47 | ||
|
|
b32247eff4 | ||
|
|
da2981a5be | ||
|
|
081210d3ae | ||
|
|
778490f090 | ||
|
|
daf0457175 | ||
|
|
f313e9edfa | ||
|
|
a209038263 | ||
|
|
d0b6444332 | ||
|
|
09cfddef94 | ||
|
|
804e17dbe6 | ||
|
|
e003cbb11f | ||
|
|
2c2bf379a8 | ||
|
|
aa48304220 | ||
|
|
3203817e51 | ||
|
|
8d98c134d0 | ||
|
|
fbcbf008ea | ||
|
|
16c69ac7df | ||
|
|
c1ebc80235 | ||
|
|
f84d8d137b | ||
|
|
4b75e4b491 | ||
|
|
793d3f61a3 | ||
|
|
7c8831501a | ||
|
|
7c2e0db615 | ||
|
|
408566b6ea | ||
|
|
5cdbf51c1e | ||
|
|
61c3d2f6c2 | ||
|
|
5e3a289f4e | ||
|
|
cc1f9653de | ||
|
|
507ff7347d | ||
|
|
709d5d2b41 | ||
|
|
f27c172a73 | ||
|
|
14c7e69172 | ||
|
|
f5e6920e92 | ||
|
|
566f76459f | ||
|
|
fe68810e0f | ||
|
|
0dca4be1f3 | ||
|
|
c310e57035 | ||
|
|
05cb3b1b9f | ||
|
|
83e6d7ee22 | ||
|
|
2044545136 | ||
|
|
973108ab41 | ||
|
|
b37ee0bd27 | ||
|
|
57d5cf3d34 | ||
|
|
dbd6dc9e54 | ||
|
|
cff21a4dab | ||
|
|
74a3cfe8ed | ||
|
|
3e258c6afe | ||
|
|
884ae1f841 | ||
|
|
7a4e422bd5 | ||
|
|
d87d87b5a3 | ||
|
|
8379439eed | ||
|
|
346aa2e8b6 | ||
|
|
4ba8088373 | ||
|
|
0d5f084cd2 | ||
|
|
e3011ec767 | ||
|
|
286ea0252c | ||
|
|
dff0c12c77 | ||
|
|
44f30718ac | ||
|
|
84bf2277de | ||
|
|
ece9132ca1 | ||
|
|
a56a4dfbf6 | ||
|
|
c6aa1ac6fb | ||
|
|
6e9e150253 | ||
|
|
8b7f724b2a | ||
|
|
46b690deb0 | ||
|
|
3e6cf27505 | ||
|
|
b3c06c0a80 | ||
|
|
9c379af76b | ||
|
|
be436085b3 | ||
|
|
84dfdf5210 | ||
|
|
24736d0577 | ||
|
|
7db38ae9f4 | ||
|
|
7ed4fc67d5 | ||
|
|
9b5727ca45 | ||
|
|
830358abbb |
No files matched your search
Executable
+22
@@ -0,0 +1,22 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
/baclup/
|
||||
.~*
|
||||
*.xlsx
|
||||
*.log
|
||||
.idea
|
||||
uv.lock
|
||||
youtubelist.txt
|
||||
*.mkv
|
||||
*.mp4
|
||||
.spotdl-cache
|
||||
script_template.txt
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
3.12
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
from concurrent.futures import ProcessPoolExecutor, as_completed
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
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):
|
||||
with open(logfile, "a") as log:
|
||||
log.write(f"{message}\n")
|
||||
print(message)
|
||||
|
||||
def get_duration_ms(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)
|
||||
|
||||
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(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_final = book_dir / f"{book_dir.name}.m4b"
|
||||
|
||||
if output_file_final.exists():
|
||||
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(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"
|
||||
|
||||
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 media_files:
|
||||
cf.write(f"file '{ffmpeg_escape(media)}'\n")
|
||||
|
||||
duration_ms = 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(media_files[0]),
|
||||
# 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(media_files[0]),
|
||||
# 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),
|
||||
]
|
||||
with logfile.open("a", encoding="utf-8") as log:
|
||||
result = subprocess.run(
|
||||
cmd_default,
|
||||
stdout=log,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 234:
|
||||
log_message(f"{YELLOW}[WARNING]{RESET} - {book_dir.name} - {result.returncode} retrying with aac encoder")
|
||||
result = subprocess.run(
|
||||
cmd_backup,
|
||||
stdout=log,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
output_file_tmp.rename(output_file_final)
|
||||
for media in media_files:
|
||||
media.unlink()
|
||||
log_message(f"{GREEN}[SUCCESS]{RESET} - {book_dir.name} - delete media files")
|
||||
else:
|
||||
log_message(f"{RED}[ERROR]{RESET} - {book_dir.name} - Code: {result.returncode}, Stderr: {result.stderr}")
|
||||
|
||||
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()
|
||||
parser.add_argument(
|
||||
"path",
|
||||
type=Path,
|
||||
help="Book folder or root folder",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
target = args.path.resolve()
|
||||
|
||||
if not target.exists():
|
||||
raise SystemExit(f"Path does not exist: {target}")
|
||||
|
||||
# Direct book folder
|
||||
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, 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
|
||||
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__":
|
||||
main()
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
cd $1
|
||||
for d in */; do
|
||||
album="${d%/}"
|
||||
|
||||
# Remove leading numbering: "001 - "
|
||||
album=$(echo "$album" | sed -E 's/^[0-9]+[[:space:]]*-[[:space:]]*//')
|
||||
album=$(echo "$album" | sed -E 's/^\([^)]*\)[[:space:]]*//; s/[[:space:]]*\([^)]*\)$//')
|
||||
# Remove all (...) groups
|
||||
album=$(echo "$album" | sed -E 's/$$[^)]*$$//g')
|
||||
|
||||
# Remove all [...] groups
|
||||
album=$(echo "$album" | sed -E 's/$$[^]]*$$//g')
|
||||
|
||||
# Remove standalone years
|
||||
album=$(echo "$album" | sed -E 's/[[:space:]]+[12][0-9]{3}//g')
|
||||
|
||||
# Collapse multiple spaces and trim
|
||||
album=$(echo "$album" | sed -E 's/[[:space:]]+/ /g; s/^ //; s/ $//')
|
||||
|
||||
echo "Setting album: $album"
|
||||
|
||||
cd "$d" || continue
|
||||
|
||||
for x in *; do
|
||||
[ -f "$x" ] || continue
|
||||
kid3-cli -c "set album \"$album\"" -c save "$x"
|
||||
done
|
||||
|
||||
cd ..
|
||||
done
|
||||
|
||||
+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()
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
root="$1"
|
||||
|
||||
find "$root" -type f \( \
|
||||
-iname '*.m4b' -o \
|
||||
-iname '*.m4a' -o \
|
||||
-iname '*.mp3' -o \
|
||||
-iname '*.aac' -o \
|
||||
-iname '*.flac' -o \
|
||||
-iname '*.ogg' -o \
|
||||
-iname '*.opus' -o \
|
||||
-iname '*.wav' -o \
|
||||
-iname '*.wma' -o \
|
||||
-iname '*.mp4' \
|
||||
\) -print0 |
|
||||
while IFS= read -r -d '' file; do
|
||||
rel="${file#"$root"/}"
|
||||
|
||||
# Only process: book/file
|
||||
# Leave already organized: author/book/file
|
||||
[[ "$rel" == */*/* ]] && continue
|
||||
[[ "$rel" != */* ]] && continue
|
||||
|
||||
book="${rel%%/*}"
|
||||
name="${rel#*/}"
|
||||
|
||||
artist="$(ffprobe -v error \
|
||||
-show_entries format_tags=artist \
|
||||
-of default=noprint_wrappers=1:nokey=1 \
|
||||
"$file" 2>/dev/null || true)"
|
||||
|
||||
# Trim whitespace
|
||||
artist="$(printf '%s' "$artist" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
||||
|
||||
# No artist tag -> skip
|
||||
[[ -z "$artist" ]] && continue
|
||||
|
||||
dest="$root/$artist/$book"
|
||||
|
||||
mkdir -p -- "$dest"
|
||||
|
||||
echo "Moving: $rel"
|
||||
echo " -> $artist/$book/$name"
|
||||
|
||||
mv -- "$file" "$dest/$name"
|
||||
done
|
||||
|
||||
|
||||
# Remove empty directories
|
||||
find "$root" -depth -type d -empty -delete
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
#!/bin/bash
|
||||
|
||||
###############################################################################
|
||||
# Script Name: add-mp3-cover.sh
|
||||
#
|
||||
# Description:
|
||||
# Embeds album artwork into MP3 files using either ffmpeg or lame.
|
||||
#
|
||||
# By default, the script processes all MP3 files in the current directory.
|
||||
# Alternatively, a single MP3 file can be specified.
|
||||
#
|
||||
# When using ffmpeg:
|
||||
# - Preserves existing audio stream.
|
||||
# - Embeds the supplied image as the front album cover.
|
||||
# - Writes ID3v2.3-compatible metadata.
|
||||
#
|
||||
# When using lame:
|
||||
# - Uses the --ti option to attach album artwork.
|
||||
#
|
||||
# Requirements:
|
||||
# - ffmpeg (recommended)
|
||||
# - or lame
|
||||
# - Cover image located in:
|
||||
# /home/lucas/Downloads/
|
||||
#
|
||||
# Usage:
|
||||
# Process all MP3 files:
|
||||
# ./add-mp3-cover.sh cover.jpg
|
||||
#
|
||||
# Process a specific MP3 file:
|
||||
# ./add-mp3-cover.sh cover.jpg song.mp3
|
||||
#
|
||||
# Examples:
|
||||
# Add cover.jpg to every MP3 in current directory:
|
||||
# ./add-mp3-cover.sh cover.jpg
|
||||
#
|
||||
# Add cover.png to a specific track:
|
||||
# ./add-mp3-cover.sh cover.png "My Song.mp3"
|
||||
#
|
||||
# Notes:
|
||||
# - Original files are replaced with updated versions.
|
||||
# - ffmpeg mode is enabled by default.
|
||||
# - Cover image is read from /home/lucas/Downloads/.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
FFMPEG="true"
|
||||
|
||||
if [ "$2" == "" ]; then
|
||||
|
||||
# Process all MP3 files in current directory
|
||||
for file in *.mp3; do
|
||||
|
||||
if [[ "${FFMPEG}" == "true" ]]; then
|
||||
|
||||
ffmpeg \
|
||||
-i "$file" \
|
||||
-i "/home/lucas/Downloads/$1" \
|
||||
-map 0:0 \
|
||||
-map 1:0 \
|
||||
-c copy \
|
||||
-id3v2_version 3 \
|
||||
-metadata:s:v title="Album cover" \
|
||||
-metadata:s:v comment="Cover (front)" \
|
||||
"$file.mp3"
|
||||
|
||||
else
|
||||
|
||||
lame --ti "/home/lucas/Downloads/$1" "$file"
|
||||
|
||||
fi
|
||||
|
||||
mv "$file.mp3" "$file"
|
||||
|
||||
done
|
||||
|
||||
else
|
||||
|
||||
# Process a specific MP3 file
|
||||
for file in "$2"; do
|
||||
|
||||
if [[ "${FFMPEG}" == "true" ]]; then
|
||||
|
||||
ffmpeg \
|
||||
-i "$file" \
|
||||
-i "/home/lucas/Downloads/$1" \
|
||||
-map 0:0 \
|
||||
-map 1:0 \
|
||||
-c copy \
|
||||
-id3v2_version 3 \
|
||||
-metadata:s:v title="Album cover" \
|
||||
-metadata:s:v comment="Cover (front)" \
|
||||
"$file.mp3"
|
||||
|
||||
else
|
||||
|
||||
lame --ti "/home/lucas/Downloads/$1" "$file"
|
||||
|
||||
fi
|
||||
|
||||
mv "$file.mp3" "$file"
|
||||
|
||||
done
|
||||
|
||||
fi
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <Series> [additional ffmpeg_handling.py args...]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
series="$1"
|
||||
shift
|
||||
|
||||
for d in */; do
|
||||
[ -d "$d" ] || continue
|
||||
name="${d%/}"
|
||||
|
||||
# extract leading number (int or float)
|
||||
if [[ $name =~ ([0-9]+([.,][0-9]+)?) ]]; then
|
||||
num="${BASH_REMATCH[1]}"
|
||||
else
|
||||
continue
|
||||
fi
|
||||
echo Folder: $d, Number: $num
|
||||
/home/lucas/scripts/.venv/bin/python \
|
||||
/home/lucas/scripts/ffmpeg_handling.py \
|
||||
ms "$name" \
|
||||
--override \
|
||||
-k grouping \
|
||||
-v "$series #$num" \
|
||||
"$@" > /home/lucas/metadata_set.log
|
||||
# --kidflag \
|
||||
done
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
set -Eeuo pipefail
|
||||
escape_kid3() {
|
||||
printf '%s' "$1" | sed "s/'/'\\\\''/g"
|
||||
}
|
||||
DRY_RUN=false
|
||||
|
||||
if [[ "$1" == "--dry-run" ]]; then
|
||||
DRY_RUN=true
|
||||
shift
|
||||
fi
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "Usage: $0 [--dry-run] file.txt"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
list="$1"
|
||||
|
||||
if [[ ! -f "$list" ]]; then
|
||||
echo "ERROR: File list not found: $list"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
while IFS= read -r file; do
|
||||
|
||||
[[ -z "$file" ]] && continue
|
||||
|
||||
if [[ ! -e "$file" ]]; then
|
||||
echo "ERROR: Missing file: $file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -d "$file" ]]; then
|
||||
echo "WARNING: Is a directory, skipping: $file"
|
||||
continue
|
||||
fi
|
||||
|
||||
bookdir="$(dirname "$file")"
|
||||
booktitle="$(basename "$bookdir")"
|
||||
author="$(basename "$(dirname "$bookdir")")"
|
||||
|
||||
echo
|
||||
echo "File: $file"
|
||||
echo "Artist: $author"
|
||||
echo "Album: $booktitle"
|
||||
|
||||
if $DRY_RUN; then
|
||||
echo "DRY RUN: no changes made"
|
||||
continue
|
||||
fi
|
||||
|
||||
artist=$(escape_kid3 "$author")
|
||||
album=$(escape_kid3 "$booktitle")
|
||||
|
||||
kid3-cli "$file" \
|
||||
-c "tag 1" \
|
||||
-c "set artist '$artist'" \
|
||||
-c "set album '$album'" \
|
||||
-c "tag 2" \
|
||||
-c "set artist '$artist'" \
|
||||
-c "set album '$album'" \
|
||||
-c save
|
||||
done < "$list"
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
|
||||
BASE="${1:-.}"
|
||||
|
||||
for dir in "$BASE"/*/; do
|
||||
[ -d "$dir" ] || continue
|
||||
|
||||
title=$(basename "$dir")
|
||||
|
||||
# Skip directories starting with a number
|
||||
[[ "$title" =~ ^[0-9] ]] && continue
|
||||
|
||||
# Author = parent directory of book directory
|
||||
author=$(basename "$(dirname "$dir")")
|
||||
|
||||
# Fix author name formatting
|
||||
author="${author//JRR/J. R. R.}"
|
||||
|
||||
for pdf in "$dir"/*.pdf; do
|
||||
[ -f "$pdf" ] || continue
|
||||
|
||||
opf="$dir/metadata.opf"
|
||||
|
||||
cat > "$opf" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<package xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:opf="http://www.idpf.org/2007/opf"
|
||||
version="2.0">
|
||||
<metadata>
|
||||
<dc:title>$title</dc:title>
|
||||
<dc:creator opf:role="aut">$author</dc:creator>
|
||||
<dc:language>en</dc:language>
|
||||
<dc:format>application/pdf</dc:format>
|
||||
</metadata>
|
||||
</package>
|
||||
EOF
|
||||
|
||||
echo "Created: $opf"
|
||||
done
|
||||
done
|
||||
+172
@@ -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()
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from difflib import SequenceMatcher
|
||||
|
||||
AUDIO_EXTENSIONS = {
|
||||
".mp3",
|
||||
".m4b",
|
||||
".m4a",
|
||||
}
|
||||
|
||||
RED = "\033[31m"
|
||||
GREEN = "\033[32m"
|
||||
RESET = "\033[0m"
|
||||
USE_KID3 = True
|
||||
def normalize(text: str) -> str:
|
||||
return re.sub(r"[^a-z0-9]", "", text.lower())
|
||||
|
||||
|
||||
def get_tag(path, tag):
|
||||
if USE_KID3:
|
||||
result = subprocess.run(
|
||||
["kid3-cli", "-c", f"get {tag}", str(path)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
# id3v2 frame mapping
|
||||
frames = {
|
||||
"album": "TALB",
|
||||
"artist": "TPE1",
|
||||
"title": "TIT2",
|
||||
"year": "TYER",
|
||||
"grouping": "TIT1", # Content group description
|
||||
"work": "TIT1", # No separate WORK frame in id3v2
|
||||
}
|
||||
|
||||
frame = frames[tag]
|
||||
|
||||
result = subprocess.run(
|
||||
["id3v2", "-R", str(path)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
for line in result.stdout.splitlines():
|
||||
if line.startswith(frame):
|
||||
return line.split(":", 1)[1].strip()
|
||||
|
||||
return ""
|
||||
|
||||
def tag_file(path: Path, rank: int, year: str, dry_run: bool, replace_pattern: str=r"Top 100 SciFi Books" ):
|
||||
work = get_tag(path, "work")
|
||||
grouping = get_tag(path, "grouping")
|
||||
existing_year = get_tag(path, "year")
|
||||
|
||||
if work:
|
||||
tag = "work"
|
||||
existing = work
|
||||
elif grouping:
|
||||
tag = "grouping"
|
||||
existing = grouping
|
||||
else:
|
||||
tag = "grouping"
|
||||
existing = ""
|
||||
pattern = rf"{replace_pattern} #\d+(?:\.\d+)?"
|
||||
|
||||
if existing:
|
||||
if re.search(pattern, existing):
|
||||
new_group = re.sub(
|
||||
pattern,
|
||||
f"{replace_pattern} #{rank};",
|
||||
existing,
|
||||
)
|
||||
else:
|
||||
new_group = f"{existing}; {replace_pattern} #{rank};"
|
||||
else:
|
||||
new_group = f"{replace_pattern} #{rank};"
|
||||
|
||||
new_group = re.sub(r";+", ";", new_group)
|
||||
# Skip if nothing changes
|
||||
if existing == new_group and existing_year == str(year):
|
||||
print("Skipping (already tagged)")
|
||||
return "skip"
|
||||
print(f"New tag {path.name}: {new_group}, Year: {year}")
|
||||
|
||||
if dry_run:
|
||||
return
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
"kid3-cli",
|
||||
"-c", f'set {tag} "{new_group}"',
|
||||
"-c", f"set year {year}",
|
||||
str(path),
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument("library")
|
||||
parser.add_argument("booklist")
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
parser.add_argument("--find-missing", action="store_true")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
library = Path(args.library)
|
||||
replace_pattern=Path(args.booklist).stem
|
||||
found = set()
|
||||
|
||||
with open(args.booklist, encoding="utf-8") as f:
|
||||
books = [
|
||||
line.strip()
|
||||
for line in f
|
||||
if line.strip() and not line.startswith("#")
|
||||
]
|
||||
|
||||
author_dirs = {
|
||||
normalize(p.name): p
|
||||
for p in library.iterdir()
|
||||
if p.is_dir()
|
||||
}
|
||||
rank_list=[]
|
||||
# print(books)
|
||||
for line in books:
|
||||
line = line.strip()
|
||||
|
||||
if not line:
|
||||
continue # skip empty lines
|
||||
|
||||
parts = line.split(" - ")
|
||||
|
||||
if len(parts) == 4:
|
||||
rank, title, author, year = parts
|
||||
elif len(parts) == 3:
|
||||
rank, title, author = parts
|
||||
year = None
|
||||
else:
|
||||
raise ValueError(f"Invalid line: {line}")
|
||||
author_key = normalize(author)
|
||||
title_key = normalize(title)
|
||||
rank_list.append(rank)
|
||||
author_dir = author_dirs.get(author_key)
|
||||
|
||||
if author_dir is None:
|
||||
print(f"{RED}AUTHOR{RESET} {author} ")
|
||||
continue
|
||||
|
||||
book_dir = None
|
||||
best_score = 0.0
|
||||
|
||||
for subdir in author_dir.rglob("*"):
|
||||
if not subdir.is_dir():
|
||||
continue
|
||||
score = SequenceMatcher(
|
||||
None,
|
||||
title_key,
|
||||
normalize(subdir.name),
|
||||
).ratio()
|
||||
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
book_dir = subdir
|
||||
|
||||
if best_score < 0.6:
|
||||
book_dir = None
|
||||
print(f"{RED}TITLE{RESET} {title}, Score {best_score} ")
|
||||
for subdir in author_dir.rglob("*"):
|
||||
if not subdir.is_dir():
|
||||
continue
|
||||
else:
|
||||
print(subdir)
|
||||
|
||||
continue
|
||||
|
||||
found.add(rank)
|
||||
|
||||
print(f"{GREEN}FOUND{RESET} #{rank:3} {title} in {book_dir}")
|
||||
|
||||
if args.find_missing:
|
||||
continue
|
||||
|
||||
for file in book_dir.iterdir():
|
||||
if file.suffix.lower() not in AUDIO_EXTENSIONS:
|
||||
continue
|
||||
|
||||
ret=tag_file(file, rank, year, args.dry_run, replace_pattern)
|
||||
if ret == "skip":
|
||||
break
|
||||
#print("next")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
#!/bin/bash
|
||||
|
||||
DRY_RUN=false
|
||||
DELETE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
;;
|
||||
--delete)
|
||||
DELETE=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "Usage: $0 [--dry-run] [--delete] file.txt"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
list="$1"
|
||||
|
||||
while IFS= read -r file; do
|
||||
book
|
||||
[[ -z "$file" ]] && continue
|
||||
|
||||
if [[ ! -e "$file" ]]; then
|
||||
echo "ERROR: Missing file: $file"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -d "$file" ]]; then
|
||||
echo "WARNING: Is a directory, skipping: $file"
|
||||
continue
|
||||
fi
|
||||
# Get comment from ID3v2
|
||||
comment=$(kid3-cli "$file" \
|
||||
-c "tag 2" \
|
||||
-c "get comment" 2>/dev/null)
|
||||
|
||||
[[ -z "$comment" ]] && continue
|
||||
|
||||
echo
|
||||
echo "File: $file"
|
||||
echo "Comment: $comment"
|
||||
|
||||
composer=""
|
||||
|
||||
# Match: read by Name; text
|
||||
if [[ "$comment" =~ ^[Rr]ead[[:space:]]+[Bb]y[[:space:]]+([^;]+)\; ]]; then
|
||||
composer="${BASH_REMATCH[1]}"
|
||||
composer="$(echo "$composer" | sed 's/[[:space:]]*$//')"
|
||||
|
||||
echo "Found narrator: $composer"
|
||||
|
||||
if $DRY_RUN; then
|
||||
echo "DRY RUN: set composer '$composer', delete comment"
|
||||
else
|
||||
kid3-cli "$file" \
|
||||
-c "tag 2" \
|
||||
-c "set composer '$composer'" \
|
||||
-c "set comment ''" \
|
||||
-c save
|
||||
fi
|
||||
|
||||
elif [[ ${#comment} -lt 40 ]]; then
|
||||
|
||||
echo "Short comment (${#comment} chars): $comment"
|
||||
|
||||
if $DELETE; then
|
||||
if $DRY_RUN; then
|
||||
echo "DRY RUN: delete comment"
|
||||
else
|
||||
kid3-cli "$file" \
|
||||
-c "tag 2" \
|
||||
-c "set comment ''" \
|
||||
-c save
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
done < "$list"
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
WORKDIR="${1:-.}"
|
||||
|
||||
cd "$WORKDIR"
|
||||
|
||||
# Use $2 if provided, otherwise use current directory name
|
||||
AUTHOR="${2:-$(basename "$PWD")}"
|
||||
|
||||
for d in "$AUTHOR - "*; do
|
||||
[[ -d "$d" ]] || continue
|
||||
|
||||
new_name="${d#"$AUTHOR - "}"
|
||||
|
||||
echo "Renaming: $d -> $new_name"
|
||||
mv -- "$d" "$new_name"
|
||||
done
|
||||
|
||||
Executable
+104
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
LIBRARY="$1"
|
||||
MODE="${2:-}"
|
||||
REPORT="${3:-/home/lucas/audiobook_genre_cleanup.txt}"
|
||||
|
||||
if [[ -z "$LIBRARY" ]]; then
|
||||
echo "Usage: $0 <library_path> [--delete] [report_file]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mapfile -t FILES < <(
|
||||
find "$LIBRARY" -type f \( \
|
||||
-iname "*.mp3" -o \
|
||||
-iname "*.m4b" -o \
|
||||
-iname "*.m4a" \
|
||||
\)
|
||||
)
|
||||
|
||||
TOTAL=${#FILES[@]}
|
||||
PROCESSED=0
|
||||
MATCHES=0
|
||||
MODIFIED=0
|
||||
|
||||
{
|
||||
echo "Audiobook Genre Cleanup Report"
|
||||
echo "Generated: $(date)"
|
||||
echo "Library: $LIBRARY"
|
||||
echo "Total files: $TOTAL"
|
||||
echo
|
||||
} > "$REPORT"
|
||||
|
||||
for file in "${FILES[@]}"; do
|
||||
((PROCESSED++))
|
||||
|
||||
if (( PROCESSED % 100 == 0 )); then
|
||||
echo "Progress: $PROCESSED/$TOTAL files processed"
|
||||
fi
|
||||
genre=$(kid3-cli -c "get genre" "$file" 2>/dev/null)
|
||||
|
||||
[[ -z "$genre" ]] && continue
|
||||
cleaned=$(printf '%s\n' "$genre" | sed -E '
|
||||
s/[Aa]udio[[:space:]]*[Bb]ooks?//g
|
||||
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
|
||||
# Normalize genre names
|
||||
s/\b(Sci[ -]?Fi|Science Fiction|SF)\b/SciFi/Ig
|
||||
s/\bnonfiction\b/NonFiction/Ig
|
||||
|
||||
# Normalize separators (&, :, / -> ;)
|
||||
s/\b(Sci[ -]?Fi|Science Fiction|SF)\b/Science Fiction/Ig
|
||||
s/[[:space:]]*[,&:\/][[:space:]]*/;/g
|
||||
|
||||
# Normalize commas
|
||||
s/[[:space:]]*;[[:space:]]*/; /g
|
||||
|
||||
# Trim leading/trailing punctuation and whitespace
|
||||
s/^[[:space:],;:-]+//
|
||||
s/[[:space:],;-]+$//
|
||||
')
|
||||
# s/.*//;
|
||||
|
||||
if [[ "$genre" != "$cleaned" ]]; then
|
||||
((MATCHES++))
|
||||
|
||||
status="MATCH"
|
||||
|
||||
if [[ "$MODE" == "--delete" ]]; then
|
||||
kid3-cli \
|
||||
-c "tag 2" \
|
||||
-c "set genre \"$cleaned\"" \
|
||||
-c save \
|
||||
"$file"
|
||||
((MODIFIED++))
|
||||
status="UPDATED"
|
||||
fi
|
||||
|
||||
printf '[%d/%d] [%s] %s | OLD="%s" | NEW="%s"\n' \
|
||||
"$PROCESSED" "$TOTAL" "$status" "$file" "$genre" "$cleaned"
|
||||
|
||||
printf '[%d/%d] [%s] %s | OLD="%s" | NEW="%s"\n' \
|
||||
"$PROCESSED" "$TOTAL" "$status" "$file" "$genre" "$cleaned" >> "$REPORT"
|
||||
fi
|
||||
|
||||
done
|
||||
|
||||
{
|
||||
echo
|
||||
echo "========== SUMMARY =========="
|
||||
echo "Files scanned : $TOTAL"
|
||||
echo "Matches found : $MATCHES"
|
||||
echo "Files updated : $MODIFIED"
|
||||
} >> "$REPORT"
|
||||
|
||||
echo
|
||||
echo "========== SUMMARY =========="
|
||||
echo "Files scanned : $TOTAL"
|
||||
echo "Matches found : $MATCHES"
|
||||
echo "Files updated : $MODIFIED"
|
||||
echo "Report written to: $REPORT"
|
||||
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
shopt -s globstar nullglob
|
||||
|
||||
root="${1:-.}"
|
||||
|
||||
for file in "$root"/**/*; do
|
||||
[[ -f "$file" ]] || continue
|
||||
|
||||
case "${file##*.}" in
|
||||
epub|EPUB)
|
||||
continue
|
||||
;;
|
||||
nfo|NFO)
|
||||
grouping="$(grep -i 'Series Name:' "$file" 2>/dev/null | sed 's/^[Ss]eries:[[:space:]]*//')"
|
||||
;;
|
||||
m4b|M4B|m4a|M4A|mp3|MP3|aac|AAC|flac|FLAC)
|
||||
grouping="$(kid3-cli -c "get grouping" "$file" 2>/dev/null)"
|
||||
;;
|
||||
*)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ ! "$grouping" =~ Publication ]]; then
|
||||
echo Grouping: $grouping
|
||||
echo "$file"
|
||||
fi
|
||||
done
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
root="${1:-.}"
|
||||
mode="${2:-}"
|
||||
THRESHOLD=40
|
||||
find "$root" \( -iname '*.mp3' -o -iname '*.m4a' -o -iname '*.m4b' \) -print0 |
|
||||
while IFS= read -r -d '' f; do
|
||||
|
||||
comment=$(ffprobe -v error \
|
||||
-show_entries format_tags=comment \
|
||||
-of default=nw=1:nk=1 \
|
||||
-- "$f")
|
||||
|
||||
description=$(ffprobe -v error \
|
||||
-show_entries format_tags=description \
|
||||
-of default=nw=1:nk=1 \
|
||||
-- "$f")
|
||||
# trim comment
|
||||
comment="${comment#"${comment%%[![:space:]]*}"}"
|
||||
comment="${comment%"${comment##*[![:space:]]}"}"
|
||||
|
||||
# ---- DELETE MODE ----
|
||||
if [[ "$mode" == "--delete" && -n "$comment" && ${#comment} -lt $THRESHOLD ]]; then
|
||||
tmp=$(mktemp -p "$(dirname -- "$f")" ".fftmp.XXXXXX.${f##*.}") || continue
|
||||
|
||||
echo "Removing short comment: $f"
|
||||
echo " comment: $comment"
|
||||
|
||||
ffmpeg -nostdin -v error -y \
|
||||
-i "$f" \
|
||||
-map 0 \
|
||||
-map_metadata 0 \
|
||||
-c copy \
|
||||
-metadata comment= \
|
||||
"$tmp" &&
|
||||
mv -f -- "$tmp" "$f" || {
|
||||
echo "FAILED: $f" >&2
|
||||
rm -f -- "$tmp"
|
||||
}
|
||||
|
||||
continue
|
||||
fi
|
||||
|
||||
# ---- SCAN MODE ----
|
||||
# only show short non-empty tags (<20 chars)
|
||||
for tag in comment description; do
|
||||
value="${!tag}"
|
||||
|
||||
value="${value#"${value%%[![:space:]]*}"}"
|
||||
value="${value%"${value##*[![:space:]]}"}"
|
||||
|
||||
if [[ -n "$value" && ${#value} -lt $THRESHOLD ]]; then
|
||||
printf "%s\n %s: %s\n\n" "$f" "$tag" "$value"
|
||||
fi
|
||||
done
|
||||
|
||||
done
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
LIBRARY="${1:-.}"
|
||||
mapfile -t FILES < <(
|
||||
find "$LIBRARY" -type f \( \
|
||||
-iname "*.mp3" -o \
|
||||
-iname "*.m4b" -o \
|
||||
-iname "*.m4a" \
|
||||
\)
|
||||
)
|
||||
TOTAL=${#FILES[@]}
|
||||
PROCESSED=0
|
||||
for f in "${FILES[@]}"; do
|
||||
((PROCESSED++))
|
||||
if (( PROCESSED % 100 == 0 )); then
|
||||
echo "Progress: $PROCESSED/$TOTAL files processed"
|
||||
fi
|
||||
|
||||
comment=$(ffprobe -v error \
|
||||
-show_entries format_tags=comment \
|
||||
-of default=nw=1:nk=1 \
|
||||
-- "$f")
|
||||
shopt -s nocasematch
|
||||
|
||||
[[ $comment =~ (read[[:space:]]+by|spoken[[:space:]]+by|narrated[[:space:]]+by|gesprochen[[:space:]]+von|gelesen[[:space:]]+von)[[:space:]]+([^.$'\n']*) ]] || continue
|
||||
|
||||
composer="${BASH_REMATCH[2]}"
|
||||
composer="${composer%"${composer##*[!.,[:space:]]}"}"
|
||||
printf '[%d/%d] [%s] %s | OLD="%s" | NEW="%s
|
||||
"\n' "$PROCESSED" "$TOTAL" "Update" "$f" "$comment" "$composer"
|
||||
|
||||
dir=$(dirname -- "$f")
|
||||
base=$(basename -- "$f")
|
||||
ext="${base##*.}"
|
||||
|
||||
case "${ext,,}" in
|
||||
mp3) format=mp3 ;;
|
||||
m4a|m4b) format=ipod ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
|
||||
tmp=$(mktemp -p "$dir" ".${base}.XXXXXX") || {
|
||||
echo "Couldn't create temporary file in $dir" >&2
|
||||
continue
|
||||
}
|
||||
|
||||
if ffmpeg -nostdin -v error -y \
|
||||
-i "$f" \
|
||||
-map 0 \
|
||||
-map_metadata 0 \
|
||||
-c copy \
|
||||
-metadata composer="$composer" \
|
||||
-metadata comment= \
|
||||
-f "$format" \
|
||||
"$tmp"
|
||||
then
|
||||
mv -f -- "$tmp" "$f"
|
||||
else
|
||||
echo "FAILED: $f" >&2
|
||||
rm -f -- "$tmp"
|
||||
fi
|
||||
done
|
||||
Executable
+137
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
RED=$'\e[31m'
|
||||
GREEN=$'\e[32m'
|
||||
RESET=$'\e[0m'
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage:
|
||||
$(basename "$0") <library-root> <book-list> [--dry-run|--find-missing]
|
||||
|
||||
Arguments:
|
||||
library-root Root containing author/*/book/
|
||||
book-list Text file containing:
|
||||
rank|title|author|year
|
||||
|
||||
Options:
|
||||
--dry-run Show what would be tagged without writing tags
|
||||
--find-missing List books from the ranking that were not found
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ $# -ge 2 ]] || usage
|
||||
|
||||
ROOT="$1"
|
||||
BOOKLIST="$2"
|
||||
MODE="write"
|
||||
|
||||
case "${3:-}" in
|
||||
--dry-run)
|
||||
MODE="dryrun"
|
||||
;;
|
||||
--find-missing)
|
||||
MODE="missing"
|
||||
;;
|
||||
"")
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
|
||||
[[ -d "$ROOT" ]] || {
|
||||
echo "Library not found: $ROOT" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ -f "$BOOKLIST" ]] || {
|
||||
echo "Book list not found: $BOOKLIST" >&2
|
||||
exit 1
|
||||
}
|
||||
echo "Using MODE $MODE"
|
||||
echo "Libary: $ROOT"
|
||||
echo "File: $BOOKLIST"
|
||||
|
||||
normalize() {
|
||||
echo "$1" |
|
||||
tr '[:upper:]' '[:lower:]' |
|
||||
sed -E "s/[^a-z0-9]+//g"
|
||||
}
|
||||
|
||||
declare -A FOUND
|
||||
|
||||
while IFS='|' read -r RANK TITLE AUTHOR YEAR; do
|
||||
|
||||
# Skip blank lines and comments
|
||||
[[ -z "$RANK" ]] && continue
|
||||
[[ "$RANK" =~ ^# ]] && continue
|
||||
|
||||
NTITLE=$(normalize "$TITLE")
|
||||
NAUTHOR=$(normalize "$AUTHOR")
|
||||
|
||||
MATCH=""
|
||||
|
||||
AUTHOR_PATH=$(find "$ROOT" -type d -iname "*$AUTHOR*" | head -n1)
|
||||
|
||||
[[ -z "$AUTHOR_PATH" ]] && continue
|
||||
|
||||
BOOK_PATH="$AUTHOR_PATH/book"
|
||||
[[ -d "$BOOK_PATH" ]] || continue
|
||||
|
||||
while IFS= read -r FILE; do
|
||||
FILE_NAME="${FILE##*/}"
|
||||
FILE_BASE="${FILE_NAME%.*}"
|
||||
|
||||
echo "Checking: $FILE_BASE"
|
||||
|
||||
if [[ "$(normalize "$FILE_BASE")" == "$NTITLE" ]]; then
|
||||
MATCH="$FILE"
|
||||
break
|
||||
fi
|
||||
done < <(find "$BOOK_PATH" -maxdepth 1 -type f)
|
||||
if [[ -n "$MATCH" ]]; then
|
||||
FOUND["$RANK"]=1
|
||||
|
||||
GROUP="Sci Fi Top 100 Books #$RANK"
|
||||
|
||||
case "$MODE" in
|
||||
dryrun)
|
||||
echo "Would tag:"
|
||||
echo " File : $MATCH"
|
||||
echo " Album: $GROUP"
|
||||
echo " Year : $YEAR"
|
||||
echo
|
||||
;;
|
||||
|
||||
write)
|
||||
echo "Tagging: $MATCH"
|
||||
|
||||
id3v2 \
|
||||
--TALB "$GROUP" \
|
||||
--TYER "$YEAR" \
|
||||
"$MATCH"
|
||||
;;
|
||||
|
||||
missing)
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
done < "$BOOKLIST"
|
||||
|
||||
if [[ "$MODE" == "missing" ]]; then
|
||||
while IFS='|' read -r RANK TITLE AUTHOR YEAR; do
|
||||
[[ -z "$RANK" ]] && continue
|
||||
[[ "$RANK" =~ ^# ]] && continue
|
||||
|
||||
if [[ -n "${FOUND[$RANK]:-}" ]]; then
|
||||
printf "${GREEN}FOUND${RESET} #%3d | %s | %s | %s\n" \
|
||||
"$RANK" "$TITLE" "$AUTHOR" "$YEAR"
|
||||
else
|
||||
printf "${RED}MISSING${RESET} #%3d | %s | %s | %s\n" \
|
||||
"$RANK" "$TITLE" "$AUTHOR" "$YEAR"
|
||||
fi
|
||||
done < "$BOOKLIST"
|
||||
fi
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
find . -type d | while read -r dir; do
|
||||
name=$(basename "$dir")
|
||||
|
||||
# Match Disc1, Disc01, disc02, CD1, cd01, etc.
|
||||
if [[ $name =~ (^|[[:space:]._-])([Dd]isc|[Cc][Dd])0*([0-9]+)$ ]]; then
|
||||
|
||||
cdnum="${BASH_REMATCH[3]}"
|
||||
parent=$(dirname "$dir")
|
||||
parentname=$(basename "$parent")
|
||||
|
||||
find "$dir" -maxdepth 1 -type f | while read -r file; do
|
||||
filename=$(basename "$file")
|
||||
ext="${filename##*.}"
|
||||
base="${filename%.*}"
|
||||
|
||||
# If there's only one file in the folder, omit the original basename.
|
||||
if [[ $(find "$dir" -maxdepth 1 -type f | wc -l) -eq 1 ]]; then
|
||||
newname="CD${cdnum}.${ext}"
|
||||
else
|
||||
newname="CD${cdnum}.${base}.${ext}"
|
||||
fi
|
||||
echo "$file" "$parent/$newname"
|
||||
cp "$file" "$parent/$newname"
|
||||
done
|
||||
else
|
||||
echo "$name"
|
||||
fi
|
||||
done
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ $# -lt 2 ]]; then
|
||||
echo "Usage: $0 <dir1> <dir2> [del]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
dir1="$1"
|
||||
dir2="$2"
|
||||
|
||||
find "$dir1" -mindepth 2 -maxdepth 2 -type d -printf '%P\n' |
|
||||
while IFS= read -r rel; do
|
||||
if [[ -d "$dir2/$rel" ]]; then
|
||||
# Exists in both
|
||||
if [[ "$3" == "del" ]]; then
|
||||
echo "Deleting: $dir1/$rel"
|
||||
rm -rf -- "$dir1/$rel"
|
||||
|
||||
# Remove empty author directory
|
||||
author="${rel%%/*}"
|
||||
rmdir --ignore-fail-on-non-empty "$dir1/$author" 2>/dev/null
|
||||
fi
|
||||
else
|
||||
# Only exists in $1
|
||||
echo "$rel"
|
||||
fi
|
||||
done
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Directory to start from (default is current directory if none given)
|
||||
DIR="${1:-.}"
|
||||
|
||||
# Optional prefix filter (second argument)
|
||||
PREFIX="${2:-}"
|
||||
|
||||
# JPEG/WEBP compression quality (1–100, lower = more compression)
|
||||
QUALITY=85
|
||||
|
||||
# Target max size
|
||||
MAXSIZE="1920x1040"
|
||||
|
||||
# Recursively find images and compress them
|
||||
find "$DIR" -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \) | while read -r FILE; do
|
||||
BASENAME=$(basename "$FILE")
|
||||
EXT="${FILE##*.}"
|
||||
EXT_LOWER=$(echo "$EXT" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
# Skip if prefix is set and filename doesn’t start with it
|
||||
if [ -n "$PREFIX" ] && [[ "$BASENAME" != "$PREFIX"* ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Processing: $FILE"
|
||||
|
||||
TMPFILE="$(mktemp --suffix=".$EXT_LOWER")"
|
||||
|
||||
case "$EXT_LOWER" in
|
||||
jpg|jpeg)
|
||||
convert "$FILE" -resize "$MAXSIZE"\> -strip -interlace Plane -quality $QUALITY "$TMPFILE"
|
||||
;;
|
||||
png|webp)
|
||||
convert "$FILE" -resize "$MAXSIZE"\> -strip -quality $QUALITY "$TMPFILE"
|
||||
;;
|
||||
esac
|
||||
|
||||
mv "$TMPFILE" "$FILE"
|
||||
done
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/bin/bash
|
||||
USER="lucas"
|
||||
DEST="~/Downloads"
|
||||
|
||||
get_config() {
|
||||
while [[ ${1:0:1} == - ]]; do
|
||||
echo "Param: $1"
|
||||
[[ $1 == -h ]] && {
|
||||
if [ "$2" == "" ]; then echo "Too few arguments"; usage; exit; fi
|
||||
HOST="$2"
|
||||
shift 2;
|
||||
continue;
|
||||
};
|
||||
[[ $1 == -f ]] && {
|
||||
if [ "$2" == "" ]; then echo "Too few arguments"; usage; exit; fi
|
||||
file="$2"
|
||||
shift 2;
|
||||
continue;
|
||||
};
|
||||
[[ $1 == -o ]] && {
|
||||
if [ "$2" == "" ]; then echo "Too few arguments"; usage; exit; fi
|
||||
opt="$2"
|
||||
shift 2;
|
||||
continue;
|
||||
};
|
||||
[[ $1 == -d ]] && {
|
||||
if [ "$2" == "" ]; then echo "Too few arguments"; usage; exit; fi
|
||||
DEST="$2"
|
||||
shift 2;
|
||||
continue;
|
||||
};
|
||||
[[ $1 == -p ]] && {
|
||||
DEST="~/Videos/.stuff"
|
||||
shift 1;
|
||||
continue;
|
||||
};
|
||||
[[ $1 == --help ]] && {
|
||||
usage
|
||||
exit
|
||||
};
|
||||
echo "Parameter $1 discarded"
|
||||
usage
|
||||
exit
|
||||
done
|
||||
}
|
||||
usage() {
|
||||
printf "Usage:
|
||||
-h <hostname>\tRemote Hostname
|
||||
-f <filename>\tPath of local file
|
||||
-o <options>\tGive additional Options
|
||||
-d <dest>\tChoose other Destination Folder
|
||||
-p\t\tChange Destination
|
||||
--help\t\tDisplay Usage\n"
|
||||
exit
|
||||
}
|
||||
get_config "$@"
|
||||
if [ "${HOST}" == "deluge" ]; then
|
||||
HOST="serverhost"
|
||||
USER="deluge-user"
|
||||
DEST="/home/deluge-user/Downloads/Torrents/"
|
||||
fi
|
||||
echo "Copy ${file} to ${HOST}:${DEST}"
|
||||
scp -r ${opt} "${file}" ${USER}@${HOST}:${DEST}
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
scp lucas@serverhost:/etc/update-motd.d/10-help-text ~/Documents/Dokumente/Dokumente/Projekte/Server/motd.serverhost.10-help-text
|
||||
scp lucas@raspberry:~/.profile ~/Documents/Dokumente/Dokumente/Projekte/Server/motd.raspberry.profile
|
||||
rm 0
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/expect -f
|
||||
#Copy Crontab of raspberry and serverhost
|
||||
set host "raspberry"
|
||||
array set user { 0 "root" 1 "lucas"}
|
||||
set port "1160"
|
||||
for {set j 0} {$j < 2} {incr j 1} {
|
||||
spawn ssh -p $port lucas@$host
|
||||
expect {
|
||||
"No route to host" {
|
||||
puts "spawn ssh -p $port SendEnv=TERM lucas@$host failed."
|
||||
exit 0
|
||||
}
|
||||
"Login Sucsessfull" {
|
||||
if {$user($j)=="root"} {
|
||||
send "sudo crontab -l >> crontab.buf\r"
|
||||
expect "lucas: "
|
||||
send "Gina\&1994\&Nonna\r"
|
||||
} else {
|
||||
send "crontab -l >> crontab.buf\r"
|
||||
}
|
||||
#expect "$ "
|
||||
#send "cat crontab.buf\r"
|
||||
expect "$ "
|
||||
send "scp -P 3032 crontab.buf lucas@serverhost:~/Documents/Dokumente/Dokumente/Projekte/Server/crontab.$host.$user($j)\r"
|
||||
expect "$ "
|
||||
send "rm crontab.buf\r"
|
||||
puts "crontab.$host.$user($j) created."
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/expect -f
|
||||
#Copy Crontab of raspberry and serverhost
|
||||
array set host { 0 "serverhost" 1 "raspberry"}
|
||||
array set user { 0 "root" 1 "lucas"}
|
||||
array set port { 0 "3032" 1 "1160"}
|
||||
|
||||
for {set i 0} {$i < 2} {incr i 1} {
|
||||
for {set j 0} {$j < 2} {incr j 1} {
|
||||
spawn ssh -p $port($i) lucas@$host($i)
|
||||
expect {
|
||||
"No route to host" {
|
||||
puts "spawn ssh -p $port($i) lucas@$host($i) failed."
|
||||
exit 0
|
||||
}
|
||||
"Login Sucsessfull" {
|
||||
if {$user($j)=="root"} {
|
||||
send "sudo crontab -l >> crontab.buf\r"
|
||||
expect "lucas: "
|
||||
send "Gina\&1994\&Nonna\r"
|
||||
} else {
|
||||
send "crontab -l >> crontab.buf\r"
|
||||
}
|
||||
#expect "$ "
|
||||
#send "cat crontab.buf\r"
|
||||
expect "$ "
|
||||
send "scp -P 1160 crontab.buf lucas@workhost:~/Documents/Dokumente/Dokumente/Projekte/Server/crontab.$host($i).$user($j)\r"
|
||||
expect "$ "
|
||||
send "rm crontab.buf\r"
|
||||
puts "crontab.$host($i).$user($j) created."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"metrics-addr": "0.0.0.0:9323",
|
||||
"experimental": true
|
||||
"mtu": 1420
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
services:
|
||||
audiobookshelf:
|
||||
image: ghcr.io/advplyr/audiobookshelf:latest
|
||||
container_name: audiobookshelf
|
||||
restart: unless-stopped
|
||||
|
||||
ports:
|
||||
- "13378:80"
|
||||
|
||||
volumes:
|
||||
- /data/audiobookshelf/data/audiobooks-englisch:/audiobooks
|
||||
- /data/audiobookshelf//data/audiobooks-deutsch:/audiobooks-deutsch
|
||||
- /data/audiobookshelf/data/audiobooks-kinder:/audiobooks-kinder
|
||||
- /data/audiobookshelf/data/podcasts:/podcasts
|
||||
- /docker/audiobookshelf/config:/config
|
||||
- /docker/audiobookshelf/metadata:/metadata
|
||||
networks:
|
||||
- shared-proxy-services
|
||||
- default
|
||||
|
||||
networks:
|
||||
default:
|
||||
external: true
|
||||
name: mediarr-services
|
||||
shared-proxy-services:
|
||||
external: true
|
||||
@@ -0,0 +1,89 @@
|
||||
services:
|
||||
postgresql:
|
||||
image: docker.io/library/postgres:16
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_PASSWORD: authentik
|
||||
POSTGRES_USER: authentik
|
||||
POSTGRES_DB: authentik
|
||||
volumes:
|
||||
- database:/var/lib/postgresql/data
|
||||
|
||||
server:
|
||||
image: ghcr.io/goauthentik/server:2026.8
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
AUTHENTIK_REDIS__HOST: redis
|
||||
AUTHENTIK_REDIS__PASSWORD: PerCent7heStorySp0tInOrd3r
|
||||
AUTHENTIK_REDIS__DB: 0
|
||||
AUTHENTIK_POSTGRESQL__HOST: postgresql
|
||||
AUTHENTIK_POSTGRESQL__USER: authentik
|
||||
AUTHENTIK_POSTGRESQL__NAME: authentik
|
||||
AUTHENTIK_POSTGRESQL__PASSWORD: authentik
|
||||
AUTHENTIK_SECRET_KEY: "/jpOrfgtWEoLukXgpqC0JQWgmKUqyu5dG+oUwawVsLs83Wl5dC+nALtsosyrnBAIyWefKKQMcET2yy1RwhZO5w=="
|
||||
AUTHENTIK_EMAIL__HOST: smtp.web.de
|
||||
AUTHENTIK_EMAIL__PORT: 587
|
||||
AUTHENTIK_EMAIL__USERNAME: "ll_m@web.de"
|
||||
AUTHENTIK_EMAIL__PASSWORD: "Mira2012"
|
||||
AUTHENTIK_EMAIL__USE_TLS: true
|
||||
AUTHENTIK_EMAIL__USE_SSL: false
|
||||
AUTHENTIK_EMAIL__TIMEOUT: 30
|
||||
AUTHENTIK_EMAIL__FROM: "ll_m@web.de"
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9443:9443"
|
||||
depends_on:
|
||||
- postgresql
|
||||
volumes:
|
||||
- media:/media
|
||||
- custom-templates:/templates
|
||||
networks:
|
||||
- shared-proxy-services
|
||||
- default
|
||||
|
||||
worker:
|
||||
image: ghcr.io/goauthentik/server:2026.8
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
environment:
|
||||
AUTHENTIK_REDIS__HOST: redis
|
||||
AUTHENTIK_REDIS__PASSWORD: PerCent7heStorySp0tInOrd3r
|
||||
AUTHENTIK_REDIS__DB: 0
|
||||
AUTHENTIK_POSTGRESQL__HOST: postgresql
|
||||
AUTHENTIK_POSTGRESQL__USER: authentik
|
||||
AUTHENTIK_POSTGRESQL__NAME: authentik
|
||||
AUTHENTIK_POSTGRESQL__PASSWORD: authentik
|
||||
AUTHENTIK_SECRET_KEY: "/jpOrfgtWEoLukXgpqC0JQWgmKUqyu5dG+oUwawVsLs83Wl5dC+nALtsosyrnBAIyWefKKQMcET2yy1RwhZO5w=="
|
||||
AUTHENTIK_EMAIL__HOST: smtp.web.de
|
||||
AUTHENTIK_EMAIL__PORT: 587
|
||||
AUTHENTIK_EMAIL__USERNAME: "ll_m@web.de"
|
||||
AUTHENTIK_EMAIL__PASSWORD: "Mira2012"
|
||||
AUTHENTIK_EMAIL__USE_TLS: true
|
||||
AUTHENTIK_EMAIL__USE_SSL: false
|
||||
AUTHENTIK_EMAIL__TIMEOUT: 30
|
||||
AUTHENTIK_EMAIL__FROM: "ll_m@web.de"
|
||||
depends_on:
|
||||
- postgresql
|
||||
volumes:
|
||||
- media:/media
|
||||
- certificates:/certificates
|
||||
- custom-templates:/templates
|
||||
networks:
|
||||
- shared-proxy-services
|
||||
- default
|
||||
|
||||
volumes:
|
||||
database:
|
||||
redis:
|
||||
media:
|
||||
certificates:
|
||||
custom-templates:
|
||||
|
||||
networks:
|
||||
default:
|
||||
external: true
|
||||
name: shared-redis-services
|
||||
shared-proxy-services:
|
||||
external: true
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
services:
|
||||
diun:
|
||||
image: crazymax/diun:latest
|
||||
container_name: diun
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ./data:/data
|
||||
|
||||
environment:
|
||||
TZ: Europe/Vienna
|
||||
|
||||
# General
|
||||
DIUN_WATCH_WORKERS: 20
|
||||
DIUN_WATCH_SCHEDULE: "0 0 * * *"
|
||||
DIUN_PROVIDERS_DOCKER: "true"
|
||||
DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT: "true"
|
||||
|
||||
# Email notifications
|
||||
DIUN_NOTIF_MAIL_HOST: smtp.web.de
|
||||
DIUN_NOTIF_MAIL_PORT: 587
|
||||
DIUN_NOTIF_MAIL_USERNAME: ll_m@web.de
|
||||
DIUN_NOTIF_MAIL_PASSWORD: Mira2012
|
||||
DIUN_NOTIF_MAIL_FROM: ll_m@web.de
|
||||
DIUN_NOTIF_MAIL_TO: ll_m@web.de
|
||||
DIUN_NOTIF_MAIL_INSECURESKIPVERIFY: "false"
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
services:
|
||||
gitea:
|
||||
image: gitea/gitea:latest
|
||||
container_name: gitea
|
||||
restart: always
|
||||
|
||||
environment:
|
||||
USER_UID: 1000
|
||||
USER_GID: 1000
|
||||
|
||||
ports:
|
||||
- "3001:3000"
|
||||
- "2222:22"
|
||||
|
||||
volumes:
|
||||
- /data/gitea:/data
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
services:
|
||||
homeassistant:
|
||||
container_name: homeassistant
|
||||
image: ghcr.io/home-assistant/home-assistant:stable
|
||||
restart: unless-stopped
|
||||
privileged: true
|
||||
network_mode: host
|
||||
environment:
|
||||
TZ: Europe/Vienna
|
||||
volumes:
|
||||
- /srv/homeassistant:/config
|
||||
|
||||
mosquitto:
|
||||
container_name: mosquitto
|
||||
image: eclipse-mosquitto:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "1883:1883"
|
||||
- "9001:9001"
|
||||
volumes:
|
||||
- ./mosquitto/config:/mosquitto/config
|
||||
- ./mosquitto/data:/mosquitto/data
|
||||
- ./mosquitto/log:/mosquitto/log
|
||||
@@ -0,0 +1,37 @@
|
||||
|
||||
services:
|
||||
jellyfin:
|
||||
image: jellyfin/jellyfin
|
||||
container_name: jellyfin
|
||||
volumes:
|
||||
- /docker/jellyfin/config:/config
|
||||
- /docker/jellyfin/cache:/cache
|
||||
- /data/jellyfin/media:/media
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8096:8096"
|
||||
environment:
|
||||
- TZ=Europe/Berlin
|
||||
networks:
|
||||
- default
|
||||
- shared-proxy-services
|
||||
|
||||
seerr:
|
||||
image: ghcr.io/seerr-team/seerr:latest
|
||||
container_name: seerr
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5055:5055"
|
||||
volumes:
|
||||
- /docker/jellyfin/seerr:/app/config
|
||||
environment:
|
||||
TZ: Europe/Berlin
|
||||
networks:
|
||||
- default
|
||||
- shared-proxy-services
|
||||
networks:
|
||||
default:
|
||||
external: true
|
||||
name: mediarr-services
|
||||
shared-proxy-services:
|
||||
external: true
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
|
||||
echo "Creating /buffer..."
|
||||
|
||||
mkdir -p /buffer
|
||||
chown -R abc:users /buffer
|
||||
chmod 775 /buffer
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
|
||||
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
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8990:8989"
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Europe/Berlin
|
||||
volumes:
|
||||
- ./sonarr_DE:/config
|
||||
- /data/jellyfin/media/series-german:/JellyfinSeriesGerman
|
||||
- /data/jellyfin/media/anime-german:/JellyfinAnimeGerman
|
||||
- /data/jellyfin/media/series-kids:/JellyfinKinder
|
||||
- /data/jellyfin/buffer:/buffer
|
||||
- /torrent/qbittorrent/downloads:/downloads
|
||||
- /torrent/rundfunkarr:/downloads-rundfunkarr
|
||||
radarr-en:
|
||||
image: lscr.io/linuxserver/radarr:latest
|
||||
container_name: radarr-en
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "7878:7878"
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Europe/Berlin
|
||||
volumes:
|
||||
- ./radarr_EN:/config
|
||||
- /data/jellyfin/media/movies-english:/JellyfinMoviesEnglish
|
||||
- /data/jellyfin/media/comedy-english:/JellyfinComedy
|
||||
- /torrent/qbittorrent/downloads:/downloads
|
||||
- /data/jellyfin/buffer:/buffer
|
||||
radarr-de:
|
||||
image: lscr.io/linuxserver/radarr:latest
|
||||
container_name: radarr-de
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "7879:7878"
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Europe/Berlin
|
||||
volumes:
|
||||
- ./radarr_DE:/config
|
||||
- /data/jellyfin/media/movies-german:/JellyfinMoviesGerman
|
||||
- /data/jellyfin/media/movies-kids:/JellyfinKinder
|
||||
- /data/jellyfin/buffer:/buffer
|
||||
- /torrent/qbittorrent/downloads:/downloads
|
||||
- /torrent/rundfunkarr:/downloads-rundfunkarr
|
||||
prowlarr:
|
||||
image: lscr.io/linuxserver/prowlarr:latest
|
||||
container_name: prowlarr
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9696:9696"
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Europe/Vienna
|
||||
volumes:
|
||||
- ./prowlarr:/config
|
||||
|
||||
gluetun-rundfunkarr-de:
|
||||
image: qmcgaw/gluetun
|
||||
container_name: gluetun-rundfunkarr-de
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
- ./gluetun-de:/gluetun/wireguard
|
||||
|
||||
environment:
|
||||
- VPN_SERVICE_PROVIDER=custom
|
||||
- VPN_TYPE=wireguard
|
||||
ports:
|
||||
- "6767:6767" # Rundfunkarr UI
|
||||
|
||||
rundfunkarr-de:
|
||||
image: ghcr.io/rundfunkarr/rundfunkarr:latest
|
||||
container_name: rundfunkarr-de
|
||||
environment:
|
||||
- TZ=Europe/Berlin
|
||||
- PUID=1000 # User ID (id -u)
|
||||
- PGID=1000 # Group ID (id -g)
|
||||
- DOWNLOAD_FOLDER_PATH=/downloads-rundfunkarr
|
||||
volumes:
|
||||
- ./rundfunkarr-de:/app/prisma/data
|
||||
- /torrent/rundfunkarr-de:/downloads-rundfunkarr
|
||||
# ports:
|
||||
# - 6767:6767
|
||||
restart: unless-stopped
|
||||
network_mode: "service:gluetun-rundfunkarr-de"
|
||||
depends_on:
|
||||
- gluetun-rundfunkarr-de
|
||||
|
||||
gluetun-at:
|
||||
image: qmcgaw/gluetun:latest
|
||||
container_name: gluetun-at
|
||||
restart: unless-stopped
|
||||
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
|
||||
ports:
|
||||
- "8888:8888" # HTTP Proxy
|
||||
- "1080:1080" # SOCKS5 Proxy
|
||||
environment:
|
||||
- VPN_SERVICE_PROVIDER=custom
|
||||
- VPN_TYPE=wireguard
|
||||
- SOCKS5_ENABLED=on
|
||||
- HTTPPROXY=on
|
||||
- SOCKSPROXY=on
|
||||
|
||||
- TZ=Europe/Vienna
|
||||
|
||||
volumes:
|
||||
- ./gluetun-at:/gluetun/wireguard
|
||||
# lazylibrarian:
|
||||
# image: lscr.io/linuxserver/lazylibrarian:latest
|
||||
# container_name: lazylibrarian
|
||||
# restart: unless-stopped
|
||||
# environment:
|
||||
# - PUID=1000
|
||||
# - PGID=1000
|
||||
# - TZ=Europe/Vienna
|
||||
# ports:
|
||||
# - "5299:5299"
|
||||
# volumes:
|
||||
# - ./lazylibrarian:/config
|
||||
# - /data/audiobookshelf/podcasts:/podcasts
|
||||
# - /data/audiobookshelf/audiobooks-englisch:/audiobooks
|
||||
# - /torrent/qbittorrent/downloads:/downloads
|
||||
|
||||
flaresolverr:
|
||||
image: ghcr.io/flaresolverr/flaresolverr:latest
|
||||
container_name: flaresolverr
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- LOG_LEVEL=info
|
||||
- LOG_HTML=false
|
||||
- CAPTCHA_SOLVER=none
|
||||
- TZ=Europe/Vienna
|
||||
ports:
|
||||
- "8191:8191"
|
||||
|
||||
lidarr:
|
||||
image: lscr.io/linuxserver/lidarr:latest
|
||||
container_name: lidarr
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Europe/Berlin
|
||||
volumes:
|
||||
- ./lidarr:/config
|
||||
- /data/navidrome/music:/music
|
||||
- /torrent/qbittorrent/downloads:/downloads
|
||||
|
||||
ports:
|
||||
- 8686:8686
|
||||
restart: unless-stopped
|
||||
|
||||
umlautadaptarr:
|
||||
image: pcjones/umlautadaptarr:latest
|
||||
restart: unless-stopped
|
||||
container_name: umlautadaptarr
|
||||
ports:
|
||||
- "5006:5006" # can be removed if you use the recommended prowlarr+proxy configuration
|
||||
environment:
|
||||
- TZ=Europe/Berlin
|
||||
- SONARR_0_ENABLED=true
|
||||
- SONARR_0_HOST=http://sonarr-en:8989
|
||||
- SONARR_0_APIKEY=e2005c5c204840878f8eb37d1ac6aa0e
|
||||
- RADARR_0_ENABLED=true
|
||||
- RADARR_0_HOST=http://radar-en:7878
|
||||
- RADARR_0_APIKEY=4332aee075c748f2acee41aa26f06007
|
||||
- SONARR_1_ENABLED=true
|
||||
- SONARR_1_HOST=http://sonarr-de:8990
|
||||
- SONARR_1_APIKEY=707d4fcbb3d74822ba990c6a8b7278e9
|
||||
- RADARR_1_ENABLED=true
|
||||
- RADARR_1_HOST=http://radarr-de:7879
|
||||
- RADARR_1_APIKEY=29f67f0ac8894357b22ea3f7fc1abfdf
|
||||
- LIDARR__ENABLED=true
|
||||
- LIDARR__HOST=http://lidarr:8686
|
||||
- LIDARR__APIKEY=bb41cbf2b17e439290f90ecfaeb24dff
|
||||
recyclarr:
|
||||
image: ghcr.io/recyclarr/recyclarr:8
|
||||
container_name: recyclarr
|
||||
user: 1000:1000
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=Europe/Berlin
|
||||
volumes:
|
||||
- ./recyclarr:/config
|
||||
|
||||
listenarr:
|
||||
image: ghcr.io/listenarrs/listenarr:canary
|
||||
container_name: listenarr
|
||||
ports:
|
||||
- "4545:4545"
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- UMASK=022
|
||||
# - LISTENARR_PUBLIC_URL=https://your-domain.com ## OPTIONAL: Used by Discord Bot
|
||||
volumes:
|
||||
- ./listenarr:/app/config
|
||||
- /data/audiobookshelf/data/audiobooks-englisch:/audiobooks-english
|
||||
- /data/audiobookshelf/data/audiobooks-deutsch:/audiobooks-german
|
||||
- /data/audiobookshelf/data/audiobooks-kinder:/audiobooks-kinder
|
||||
- /torrent/qbittorrent/downloads:/downloads
|
||||
restart: unless-stopped
|
||||
upload-assistant:
|
||||
image: ghcr.io/audionut/upload-assistant:latest
|
||||
container_name: upload-assistant
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
# Paths visible in the file browser
|
||||
- UA_BROWSE_ROOTS=/downloads,/data
|
||||
|
||||
command: ["--webui", "0.0.0.0:5000"]
|
||||
|
||||
volumes:
|
||||
- ./upload-assistant:/Upload-Assistant/data
|
||||
# Sonarr/Radarr libraries
|
||||
- /data/jellyfin/media/series-english:/data/series-english:ro
|
||||
- /data/jellyfin/media/anime-english:/data/anime-english:ro
|
||||
- /data/jellyfin/media/documentaries-english:/docus-english:ro
|
||||
- /data/jellyfin/media/series-german:/series-german:ro
|
||||
- /data/jellyfin/media/anime-german:/anime-german:ro
|
||||
- /data/jellyfin/media/series-kids:/series-kids:ro
|
||||
- /data/jellyfin/media/movies-english:/data/movies-english:ro
|
||||
- /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
|
||||
|
||||
networks:
|
||||
default:
|
||||
external: true
|
||||
name: mediarr-services
|
||||
@@ -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,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 = d3ldwEjnFcbLwD1o8uC5xC3DaSNek8DGeTpOb/h/IE4=
|
||||
AllowedIPs = 0.0.0.0/0
|
||||
Endpoint = 86.38.98.8:51820
|
||||
@@ -0,0 +1,9 @@
|
||||
# secrets.yml
|
||||
sonarr_en_url: http://sonarr-en:8989
|
||||
sonarr_en_apikey: e2005c5c204840878f8eb37d1ac6aa0e
|
||||
radarr_en_url: http://radarr-en:7878
|
||||
radarr_en_apikey: 4332aee075c748f2acee41aa26f06007
|
||||
sonarr_de_url: http://sonarr-de:8989
|
||||
sonarr_de_apikey: 707d4fcbb3d74822ba990c6a8b7278e9
|
||||
radarr_de_url: http://radarr-de:7878
|
||||
radarr_de_apikey: 29f67f0ac8894357b22ea3f7fc1abfdf
|
||||
@@ -0,0 +1,5 @@
|
||||
# yaml-language-server: $schema=https://schemas.recyclarr.dev/latest/settings-schema.json
|
||||
|
||||
# Edit this file to customize the behavior of Recyclarr beyond its defaults
|
||||
# For the settings file reference guide, visit the link to the wiki below:
|
||||
# https://recyclarr.dev/reference/settings/
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
sed -i 's/speed:a/speed:BigInt(Math.round(a))/g' /app/.next/server/chunks/*.js
|
||||
sed -i 's/speed,/speed: BigInt(Math.round(speed)),/' /app/src/server/download-manager.ts
|
||||
@@ -0,0 +1,70 @@
|
||||
services:
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
container_name: prometheus
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- prometheus-data:/prometheus
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
container_name: grafana
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:3000"
|
||||
|
||||
environment:
|
||||
GF_SECURITY_ADMIN_USER: lucas
|
||||
GF_SECURITY_ADMIN_PASSWORD: Mira2012
|
||||
GF_SMTP_ENABLED: "true"
|
||||
GF_SMTP_HOST: "smtp.web.de:587"
|
||||
GF_SMTP_USER: "ll_m@web.de"
|
||||
GF_SMTP_PASSWORD: "Mira2012"
|
||||
GF_SMTP_FROM_ADDRESS: "ll_m@web.de"
|
||||
GF_SMTP_FROM_NAME: "Grafana"
|
||||
GF_SMTP_SKIP_VERIFY: "false"
|
||||
GF_SERVER_ROOT_URL: https://status.someoneslinux.selfhost.eu
|
||||
GF_SERVER_PROTOCOL: http
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
- ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources
|
||||
- ./grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards
|
||||
- ./grafana/provisioning/dashboards/imported:/var/lib/grafana/dashboards
|
||||
networks:
|
||||
- default
|
||||
- shared-proxy-services
|
||||
node-exporter:
|
||||
container_name: node-exporter
|
||||
image: prom/node-exporter:latest
|
||||
restart: unless-stopped
|
||||
pid: host
|
||||
command:
|
||||
- '--path.rootfs=/host'
|
||||
- '--collector.textfile.directory=/textfile'
|
||||
volumes:
|
||||
- '/:/host:ro,rslave'
|
||||
- ./node_exporter/textfile_collector:/textfile:ro
|
||||
ports:
|
||||
- '9100:9100'
|
||||
|
||||
cadvisor:
|
||||
image: gcr.io/cadvisor/cadvisor:latest
|
||||
container_name: cadvisor
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- /:/rootfs:ro
|
||||
- /data:/data:ro
|
||||
- /docker:/docker:ro
|
||||
|
||||
volumes:
|
||||
prometheus-data:
|
||||
grafana-data:
|
||||
networks:
|
||||
default:
|
||||
shared-proxy-services:
|
||||
external: true
|
||||
@@ -0,0 +1,37 @@
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
evaluation_interval: 15s
|
||||
|
||||
scrape_configs:
|
||||
|
||||
- job_name: "prometheus"
|
||||
static_configs:
|
||||
- targets:
|
||||
- "prometheus:9090"
|
||||
|
||||
- job_name: "node-exporter"
|
||||
static_configs:
|
||||
- targets:
|
||||
- "192.168.0.2:9100"
|
||||
labels:
|
||||
host: serverhost
|
||||
- targets:
|
||||
- "node-exporter:9100"
|
||||
labels:
|
||||
host: "mediahost"
|
||||
|
||||
- job_name: "cadvisor"
|
||||
static_configs:
|
||||
- targets:
|
||||
- "cadvisor:8080"
|
||||
- job_name: "smartctl"
|
||||
static_configs:
|
||||
- targets:
|
||||
- "192.168.0.2:9633"
|
||||
labels:
|
||||
host: serverhost
|
||||
- targets:
|
||||
- "192.168.0.4:9633"
|
||||
labels:
|
||||
host: "mediahost"
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
|
||||
services:
|
||||
navidrome:
|
||||
image: deluan/navidrome:latest
|
||||
container_name: navidrome
|
||||
restart: unless-stopped
|
||||
|
||||
ports:
|
||||
- "4533:4533"
|
||||
|
||||
environment:
|
||||
ND_SCANSCHEDULE: 1h
|
||||
ND_LOGLEVEL: info
|
||||
ND_SESSIONTIMEOUT: 24h
|
||||
ND_MUSICFOLDER: /music
|
||||
ND_DATAFOLDER: /data
|
||||
ND_SCANNER_PURGEMISSING: always
|
||||
|
||||
volumes:
|
||||
- /data/navidrome/data:/data
|
||||
- /data/navidrome/music/music:/music:ro
|
||||
- /data/navidrome/music/kinder:/kinder:ro
|
||||
- /data/navidrome/music/kamerun:/kamerun:ro
|
||||
- /data/navidrome/music/weihnachten:/weihnachten:ro
|
||||
- /data/navidrome/music/playlists:/playlists:ro
|
||||
networks:
|
||||
- shared-proxy-services
|
||||
- default
|
||||
|
||||
networks:
|
||||
default:
|
||||
shared-proxy-services:
|
||||
external: true
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
(
|
||||
echo "============================================================"
|
||||
echo "NEXTCLOUD DIAGNOSTIC"
|
||||
echo "Date: $(date)"
|
||||
echo "============================================================"
|
||||
echo
|
||||
|
||||
run() {
|
||||
echo
|
||||
echo "============================================================"
|
||||
echo "COMMAND:"
|
||||
echo "$*"
|
||||
echo "============================================================"
|
||||
"$@" 2>&1
|
||||
echo
|
||||
echo "EXIT CODE: $?"
|
||||
}
|
||||
|
||||
echo "### 1. Docker containers"
|
||||
run docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}'
|
||||
|
||||
echo "### 2. Nextcloud status"
|
||||
run docker exec nextcloud php occ status
|
||||
|
||||
echo "### 3. Effective Nextcloud configuration"
|
||||
echo 'COMMAND: docker exec nextcloud php -r ...'
|
||||
docker exec nextcloud php -r '
|
||||
$CONFIG = [];
|
||||
|
||||
include "/var/www/html/config/config.php";
|
||||
|
||||
if (file_exists("/var/www/html/config/zz-docker-env.config.php")) {
|
||||
include "/var/www/html/config/zz-docker-env.config.php";
|
||||
}
|
||||
|
||||
echo "dbtype: " . ($CONFIG["dbtype"] ?? "MISSING") . PHP_EOL;
|
||||
echo "dbhost: " . ($CONFIG["dbhost"] ?? "MISSING") . PHP_EOL;
|
||||
echo "dbname: " . ($CONFIG["dbname"] ?? "MISSING") . PHP_EOL;
|
||||
echo "dbuser: " . ($CONFIG["dbuser"] ?? "MISSING") . PHP_EOL;
|
||||
echo "dbpassword: " . (empty($CONFIG["dbpassword"] ?? "") ? "EMPTY" : "SET") . PHP_EOL;
|
||||
echo "instanceid: " . ($CONFIG["instanceid"] ?? "MISSING") . PHP_EOL;
|
||||
echo "version: " . ($CONFIG["version"] ?? "MISSING") . PHP_EOL;
|
||||
echo "datadirectory: " . ($CONFIG["datadirectory"] ?? "MISSING") . PHP_EOL;
|
||||
echo "overwrite.cli.url: " . ($CONFIG["overwrite.cli.url"] ?? "MISSING") . PHP_EOL;
|
||||
echo "overwriteprotocol: " . ($CONFIG["overwriteprotocol"] ?? "MISSING") . PHP_EOL;
|
||||
|
||||
echo "redis.host: " . ($CONFIG["redis"]["host"] ?? "MISSING") . PHP_EOL;
|
||||
echo "redis.password: " . (empty($CONFIG["redis"]["password"] ?? "") ? "EMPTY" : "SET") . PHP_EOL;
|
||||
|
||||
echo "trusted_domains:" . PHP_EOL;
|
||||
foreach (($CONFIG["trusted_domains"] ?? []) as $domain) {
|
||||
echo " - " . $domain . PHP_EOL;
|
||||
}
|
||||
|
||||
echo "trusted_proxies:" . PHP_EOL;
|
||||
foreach (($CONFIG["trusted_proxies"] ?? []) as $proxy) {
|
||||
echo " - " . $proxy . PHP_EOL;
|
||||
}
|
||||
' 2>&1
|
||||
echo "EXIT CODE: $?"
|
||||
|
||||
echo
|
||||
echo "### 4. Test MariaDB connection"
|
||||
echo 'COMMAND: docker exec nextcloud php -r ...'
|
||||
docker exec nextcloud php -r '
|
||||
$CONFIG = [];
|
||||
|
||||
include "/var/www/html/config/config.php";
|
||||
|
||||
if (file_exists("/var/www/html/config/zz-docker-env.config.php")) {
|
||||
include "/var/www/html/config/zz-docker-env.config.php";
|
||||
}
|
||||
|
||||
try {
|
||||
$dsn = "mysql:host=" . $CONFIG["dbhost"] .
|
||||
";dbname=" . $CONFIG["dbname"];
|
||||
|
||||
$pdo = new PDO(
|
||||
$dsn,
|
||||
$CONFIG["dbuser"],
|
||||
$CONFIG["dbpassword"],
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
||||
]
|
||||
);
|
||||
|
||||
echo "PASS: MariaDB connection works" . PHP_EOL;
|
||||
|
||||
} catch (Throwable $e) {
|
||||
echo "FAIL: MariaDB connection failed" . PHP_EOL;
|
||||
echo $e->getMessage() . PHP_EOL;
|
||||
}
|
||||
' 2>&1
|
||||
echo "EXIT CODE: $?"
|
||||
|
||||
echo
|
||||
echo "### 5. Nextcloud config directory"
|
||||
run docker exec nextcloud ls -la /var/www/html/config
|
||||
|
||||
echo "### 6. Nextcloud root directory"
|
||||
run docker exec nextcloud ls -la /var/www/html
|
||||
|
||||
echo "### 7. Nextcloud data directory"
|
||||
run docker exec nextcloud sh -c 'ls -ld /var/www/html/data && ls -la /var/www/html/data | head -30'
|
||||
|
||||
echo "### 8. Config.php syntax"
|
||||
run docker exec nextcloud php -l /var/www/html/config/config.php
|
||||
|
||||
echo "### 9. zz-docker-env.config.php syntax"
|
||||
run docker exec nextcloud php -l /var/www/html/config/zz-docker-env.config.php
|
||||
|
||||
echo "### 10. Docker environment"
|
||||
echo 'COMMAND: docker exec nextcloud env'
|
||||
docker exec nextcloud env 2>&1 |
|
||||
sed -E \
|
||||
-e 's/(MYSQL_PASSWORD=).*/\1[REDACTED]/' \
|
||||
-e 's/(MYSQL_ROOT_PASSWORD=).*/\1[REDACTED]/' \
|
||||
-e 's/(REDIS_PASSWORD=).*/\1[REDACTED]/' \
|
||||
-e 's/(NEXTCLOUD_ADMIN_PASSWORD=).*/\1[REDACTED]/'
|
||||
echo "EXIT CODE: ${PIPESTATUS[0]}"
|
||||
|
||||
echo "### 11. Nextcloud container logs"
|
||||
run docker logs --tail 20 nextcloud
|
||||
|
||||
echo "### 12. MariaDB container logs"
|
||||
run docker logs --tail 20 nextcloud-db
|
||||
|
||||
echo "### 13. Nextcloud application log"
|
||||
echo 'COMMAND: docker exec nextcloud sh -c ...'
|
||||
docker exec nextcloud sh -c '
|
||||
if [ -f /var/www/html/data/nextcloud.log ]; then
|
||||
tail -2 /var/www/html/data/nextcloud.log
|
||||
else
|
||||
echo "nextcloud.log not found"
|
||||
fi
|
||||
' 2>&1
|
||||
echo "EXIT CODE: $?"
|
||||
|
||||
echo "### 14. Network"
|
||||
run docker inspect nextcloud --format '{{json .NetworkSettings.Networks}}'
|
||||
|
||||
echo "### 15. Database network"
|
||||
run docker inspect nextcloud-db --format '{{json .NetworkSettings.Networks}}'
|
||||
|
||||
echo "### 16. Docker Compose configuration"
|
||||
echo 'COMMAND: docker compose config'
|
||||
docker compose config 2>&1 |
|
||||
sed -E \
|
||||
-e 's/(MYSQL_PASSWORD:).*/\1 [REDACTED]/' \
|
||||
-e 's/(MYSQL_ROOT_PASSWORD:).*/\1 [REDACTED]/' \
|
||||
-e 's/(REDIS_PASSWORD:).*/\1 [REDACTED]/' \
|
||||
-e 's/(NEXTCLOUD_ADMIN_PASSWORD:).*/\1 [REDACTED]/'
|
||||
|
||||
echo "### 17. Disk space"
|
||||
run df -h
|
||||
|
||||
echo "### 18. Docker volumes/mounts"
|
||||
echo 'COMMAND: docker inspect nextcloud --format ...'
|
||||
docker inspect nextcloud \
|
||||
--format '{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}' \
|
||||
2>&1
|
||||
|
||||
echo
|
||||
echo "============================================================"
|
||||
echo "END OF DIAGNOSTIC"
|
||||
echo "============================================================"
|
||||
|
||||
) 2>&1 | tee nextcloud-diagnostic.txt
|
||||
|
||||
echo
|
||||
echo "Diagnostic saved to:"
|
||||
echo " $(pwd)/nextcloud-diagnostic.txt"
|
||||
@@ -0,0 +1,109 @@
|
||||
services:
|
||||
nextcloud:
|
||||
image: nextcloud:latest
|
||||
container_name: nextcloud
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
# PHP
|
||||
- PHP_UPLOAD_LIMIT=16G
|
||||
# APACHE2
|
||||
- APACHE_BODY_LIMIT=0
|
||||
# MYSQL
|
||||
- MYSQL_HOST=db
|
||||
- MYSQL_DATABASE=nextcloud
|
||||
- MYSQL_USER=nextcloud
|
||||
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
|
||||
# REDIS
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_HOST_PASSWORD=${REDIS_PASSWORD}
|
||||
# NEXTCLOUD
|
||||
- NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER}
|
||||
- NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD}
|
||||
- NEXTCLOUD_TRUSTED_DOMAINS=${NEXTCLOUD_DOMAIN} 192.168.0.4
|
||||
- NEXTCLOUD_MIMI_PASSWORD=${NEXTCLOUD_MIMI_PASSWORD}
|
||||
- OVERWRITEPROTOCOL=https
|
||||
- OVERWRITECLIURL=https://${NEXTCLOUD_DOMAIN}
|
||||
- OVERWRITEHOST=${NEXTCLOUD_DOMAIN}
|
||||
- TRUSTED_PROXIES=nginx 172.16.0.0/12
|
||||
# SMTP
|
||||
- SMTP_HOST=${SMTP_HOST}
|
||||
- SMTP_PORT=${SMTP_PORT}
|
||||
- SMTP_SECURE=${SMTP_SECURE}
|
||||
- SMTP_USER=${SMTP_USER}
|
||||
- SMTP_PASSWORD=${SMTP_PASSWORD}
|
||||
- SMTP_FROM_ADDRESS=${SMTP_FROM_ADDRESS}
|
||||
- SMTP_FROM_DOMAIN=${SMTP_FROM_DOMAIN}
|
||||
|
||||
# Collabora
|
||||
- COLLABORA_DOMAIN=${COLLABORA_DOMAIN}
|
||||
- LUCAS_EMAIL=${LUCAS_EMAIL}
|
||||
volumes:
|
||||
- ./html:/var/www/html
|
||||
- /data/nextcloud/data:/var/www/html/data
|
||||
# Post-installation hooks
|
||||
- ./hooks/nextcloud-init.sh:/docker-entrypoint-hooks.d/post-installation/nextcloud-init.sh:ro
|
||||
|
||||
ports:
|
||||
- "8082:80"
|
||||
networks:
|
||||
- default
|
||||
- shared-proxy-services
|
||||
- shared-redis-services
|
||||
|
||||
db:
|
||||
image: mariadb:11
|
||||
container_name: nextcloud-db
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- --transaction-isolation=READ-COMMITTED
|
||||
- --log-bin=binlog
|
||||
- --binlog-format=ROW
|
||||
- --slow-query-log=ON
|
||||
- --slow-query-log-file=/var/lib/mysql/slow-query.log
|
||||
- --long-query-time=2
|
||||
- --innodb-log-file-size=256M
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
|
||||
- MYSQL_DATABASE=nextcloud
|
||||
- MYSQL_USER=nextcloud
|
||||
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
|
||||
volumes:
|
||||
- ./db:/var/lib/mysql
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"healthcheck.sh --connect --innodb_initialized"
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
|
||||
collabora:
|
||||
image: collabora/code
|
||||
expose:
|
||||
- 9980
|
||||
cap_add:
|
||||
- MKNOD
|
||||
environment:
|
||||
- domain=${NEXTCLOUD_DOMAIN}
|
||||
- VIRTUAL_HOST=${COLLABORA_DOMAIN}
|
||||
- VIRTUAL_PORT=9980
|
||||
- VIRTUAL_PROTO=https
|
||||
- LETSENCRYPT_HOST=${COLLABORA_DOMAIN}
|
||||
- LETSENCRYPT_EMAIL=${LUCAS_EMAIL}
|
||||
networks:
|
||||
- shared-proxy-services
|
||||
restart: always
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
shared-redis-services:
|
||||
external: true
|
||||
shared-proxy-services:
|
||||
external: true
|
||||
|
||||
@@ -0,0 +1,515 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
CONTAINER="nextcloud"
|
||||
OVERRIDE="/var/www/html/config/zz-docker-env.config.php"
|
||||
|
||||
FIX=false
|
||||
|
||||
if [[ "${1:-}" == "--fix" ]]; then
|
||||
FIX=true
|
||||
fi
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
pass() {
|
||||
echo "PASS: $1"
|
||||
PASS=$((PASS + 1))
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $1"
|
||||
FAIL=$((FAIL + 1))
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Basic checks
|
||||
# ============================================================
|
||||
|
||||
echo "=========================================="
|
||||
echo " Nextcloud Docker configuration checker"
|
||||
echo "=========================================="
|
||||
echo
|
||||
|
||||
if ! docker inspect "$CONTAINER" >/dev/null 2>&1; then
|
||||
echo "FAIL: Docker container '$CONTAINER' does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER")" != "true" ]]; then
|
||||
echo "FAIL: Docker container '$CONTAINER' is not running"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f ".env" ]]; then
|
||||
echo "FAIL: .env not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# Read .env
|
||||
# ============================================================
|
||||
|
||||
declare -A ENV
|
||||
|
||||
while IFS='=' read -r key value; do
|
||||
|
||||
key="${key//$'\r'/}"
|
||||
value="${value//$'\r'/}"
|
||||
|
||||
# Ignore empty lines
|
||||
[[ -z "${key// /}" ]] && continue
|
||||
|
||||
# Ignore comments
|
||||
[[ "$key" =~ ^[[:space:]]*# ]] && continue
|
||||
|
||||
# Trim whitespace
|
||||
key="${key#"${key%%[![:space:]]*}"}"
|
||||
key="${key%"${key##*[![:space:]]}"}"
|
||||
|
||||
# Remove "export "
|
||||
key="${key#export }"
|
||||
|
||||
# Remove quotes
|
||||
value="${value#\"}"
|
||||
value="${value%\"}"
|
||||
value="${value#\'}"
|
||||
value="${value%\'}"
|
||||
|
||||
ENV["$key"]="$value"
|
||||
|
||||
done < .env
|
||||
|
||||
MYSQL_PASSWORD="${ENV[MYSQL_PASSWORD]:-}"
|
||||
MYSQL_ROOT_PASSWORD="${ENV[MYSQL_ROOT_PASSWORD]:-}"
|
||||
REDIS_PASSWORD="${ENV[REDIS_PASSWORD]:-}"
|
||||
NEXTCLOUD_ADMIN_USER="${ENV[NEXTCLOUD_ADMIN_USER]:-}"
|
||||
NEXTCLOUD_ADMIN_PASSWORD="${ENV[NEXTCLOUD_ADMIN_PASSWORD]:-}"
|
||||
NEXTCLOUD_DOMAIN="${ENV[NEXTCLOUD_DOMAIN]:-}"
|
||||
|
||||
# ============================================================
|
||||
# Environment checks
|
||||
# ============================================================
|
||||
|
||||
echo "Checking .env variables"
|
||||
echo "-----------------------"
|
||||
|
||||
for name in \
|
||||
MYSQL_PASSWORD \
|
||||
MYSQL_ROOT_PASSWORD \
|
||||
REDIS_PASSWORD \
|
||||
NEXTCLOUD_ADMIN_USER \
|
||||
NEXTCLOUD_ADMIN_PASSWORD \
|
||||
NEXTCLOUD_DOMAIN
|
||||
do
|
||||
if [[ -n "${ENV[$name]:-}" ]]; then
|
||||
pass "$name is defined"
|
||||
else
|
||||
fail "$name is missing"
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
|
||||
# ============================================================
|
||||
# Read config.php
|
||||
#
|
||||
# PHP runs ONLY inside Docker.
|
||||
# No PHP is required on the host.
|
||||
# ============================================================
|
||||
|
||||
CONFIG_JSON="$(
|
||||
docker exec "$CONTAINER" php -r '
|
||||
$CONFIG = [];
|
||||
|
||||
include "/var/www/html/config/config.php";
|
||||
|
||||
echo json_encode([
|
||||
"dbhost" =>
|
||||
$CONFIG["dbhost"] ?? "",
|
||||
|
||||
"dbname" =>
|
||||
$CONFIG["dbname"] ?? "",
|
||||
|
||||
"dbuser" =>
|
||||
$CONFIG["dbuser"] ?? "",
|
||||
|
||||
"dbpassword" =>
|
||||
$CONFIG["dbpassword"] ?? "",
|
||||
|
||||
"redis_host" =>
|
||||
$CONFIG["redis"]["host"] ?? "",
|
||||
|
||||
"redis_password" =>
|
||||
$CONFIG["redis"]["password"] ?? "",
|
||||
|
||||
"overwriteprotocol" =>
|
||||
$CONFIG["overwriteprotocol"] ?? "",
|
||||
|
||||
"overwrite_cli_url" =>
|
||||
$CONFIG["overwrite.cli.url"] ?? "",
|
||||
|
||||
"trusted_domains" =>
|
||||
$CONFIG["trusted_domains"] ?? [],
|
||||
|
||||
"trusted_proxies" =>
|
||||
$CONFIG["trusted_proxies"] ?? []
|
||||
]);
|
||||
'
|
||||
)"
|
||||
|
||||
# ============================================================
|
||||
# Extract configuration
|
||||
# ============================================================
|
||||
|
||||
DB_HOST="$(jq -r '.dbhost' <<< "$CONFIG_JSON")"
|
||||
DB_NAME="$(jq -r '.dbname' <<< "$CONFIG_JSON")"
|
||||
DB_USER="$(jq -r '.dbuser' <<< "$CONFIG_JSON")"
|
||||
DB_PASSWORD="$(jq -r '.dbpassword' <<< "$CONFIG_JSON")"
|
||||
|
||||
REDIS_HOST="$(jq -r '.redis_host' <<< "$CONFIG_JSON")"
|
||||
REDIS_PASSWORD_CONFIG="$(jq -r '.redis_password' <<< "$CONFIG_JSON")"
|
||||
|
||||
OVERWRITE_PROTOCOL="$(jq -r '.overwriteprotocol' <<< "$CONFIG_JSON")"
|
||||
OVERWRITE_CLI_URL="$(jq -r '.overwrite_cli_url' <<< "$CONFIG_JSON")"
|
||||
|
||||
# ============================================================
|
||||
# Database
|
||||
# ============================================================
|
||||
|
||||
echo "Checking database configuration"
|
||||
echo "-------------------------------"
|
||||
|
||||
if [[ "$DB_HOST" == "mariadb" ]]; then
|
||||
pass "dbhost = mariadb"
|
||||
else
|
||||
fail "dbhost: expected mariadb, got $DB_HOST"
|
||||
fi
|
||||
|
||||
if [[ "$DB_NAME" == "nextcloud" ]]; then
|
||||
pass "dbname = nextcloud"
|
||||
else
|
||||
fail "dbname: expected nextcloud, got $DB_NAME"
|
||||
fi
|
||||
|
||||
if [[ "$DB_USER" == "nextcloud" ]]; then
|
||||
pass "dbuser = nextcloud"
|
||||
else
|
||||
fail "dbuser: expected nextcloud, got $DB_USER"
|
||||
fi
|
||||
|
||||
if [[ "$MYSQL_PASSWORD" == "$DB_PASSWORD" ]]; then
|
||||
pass "dbpassword"
|
||||
else
|
||||
fail "dbpassword"
|
||||
fi
|
||||
|
||||
echo
|
||||
|
||||
# ============================================================
|
||||
# Redis
|
||||
# ============================================================
|
||||
|
||||
echo "Checking Redis configuration"
|
||||
echo "----------------------------"
|
||||
|
||||
if [[ "$REDIS_HOST" == "redis" ]]; then
|
||||
pass "redis host = redis"
|
||||
else
|
||||
fail "redis host: expected redis, got $REDIS_HOST"
|
||||
fi
|
||||
|
||||
if [[ "$REDIS_PASSWORD" == "$REDIS_PASSWORD_CONFIG" ]]; then
|
||||
pass "redis password"
|
||||
else
|
||||
fail "redis password"
|
||||
echo " expected: [hidden]"
|
||||
echo " actual: [hidden]"
|
||||
fi
|
||||
|
||||
echo
|
||||
|
||||
# ============================================================
|
||||
# URL
|
||||
# ============================================================
|
||||
|
||||
echo "Checking Nextcloud URL configuration"
|
||||
echo "------------------------------------"
|
||||
|
||||
if [[ "$OVERWRITE_PROTOCOL" == "https" ]]; then
|
||||
pass "overwriteprotocol = https"
|
||||
else
|
||||
fail "overwriteprotocol: expected https, got $OVERWRITE_PROTOCOL"
|
||||
fi
|
||||
|
||||
EXPECTED_URL="https://${NEXTCLOUD_DOMAIN}"
|
||||
|
||||
if [[ "$OVERWRITE_CLI_URL" == "$EXPECTED_URL" ]]; then
|
||||
pass "overwrite.cli.url = $EXPECTED_URL"
|
||||
else
|
||||
fail "overwrite.cli.url"
|
||||
echo " expected: $EXPECTED_URL"
|
||||
echo " actual: $OVERWRITE_CLI_URL"
|
||||
fi
|
||||
|
||||
echo
|
||||
|
||||
# ============================================================
|
||||
# Trusted domains
|
||||
# ============================================================
|
||||
|
||||
echo "Checking trusted domains"
|
||||
echo "-----------------------"
|
||||
|
||||
if jq -e \
|
||||
--arg domain "$NEXTCLOUD_DOMAIN" \
|
||||
'.trusted_domains | index($domain) != null' \
|
||||
<<< "$CONFIG_JSON" >/dev/null
|
||||
then
|
||||
pass "trusted_domains contains $NEXTCLOUD_DOMAIN"
|
||||
else
|
||||
fail "trusted_domains does not contain $NEXTCLOUD_DOMAIN"
|
||||
fi
|
||||
|
||||
echo
|
||||
|
||||
# ============================================================
|
||||
# Trusted proxies
|
||||
# ============================================================
|
||||
|
||||
echo "Checking trusted proxies"
|
||||
echo "------------------------"
|
||||
|
||||
if jq -e \
|
||||
'.trusted_proxies | index("172.16.0.0/12") != null' \
|
||||
<<< "$CONFIG_JSON" >/dev/null
|
||||
then
|
||||
pass "trusted_proxies contains 172.16.0.0/12"
|
||||
else
|
||||
fail "trusted_proxies does not contain 172.16.0.0/12"
|
||||
fi
|
||||
|
||||
echo
|
||||
|
||||
# ============================================================
|
||||
# FIX
|
||||
# ============================================================
|
||||
|
||||
if [[ "$FIX" == true ]]; then
|
||||
|
||||
echo "=========================================="
|
||||
echo " Fixing Nextcloud configuration"
|
||||
echo "=========================================="
|
||||
echo
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Backup existing override
|
||||
# --------------------------------------------------------
|
||||
|
||||
if docker exec "$CONTAINER" test -f "$OVERRIDE"; then
|
||||
|
||||
BACKUP="/var/www/html/config/zz-docker-env.config.php.backup.$(date +%Y%m%d-%H%M%S)"
|
||||
|
||||
docker exec "$CONTAINER" \
|
||||
cp "$OVERRIDE" "$BACKUP"
|
||||
|
||||
echo "Backup created:"
|
||||
echo " $BACKUP"
|
||||
echo
|
||||
fi
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Generate override INSIDE the container.
|
||||
#
|
||||
# Therefore PHP is NOT required on the host.
|
||||
# --------------------------------------------------------
|
||||
|
||||
docker exec \
|
||||
-e MYSQL_PASSWORD="$MYSQL_PASSWORD" \
|
||||
-e REDIS_PASSWORD="$REDIS_PASSWORD" \
|
||||
-e NEXTCLOUD_DOMAIN="$NEXTCLOUD_DOMAIN" \
|
||||
"$CONTAINER" \
|
||||
php -r '
|
||||
$mysql = getenv("MYSQL_PASSWORD");
|
||||
$redis = getenv("REDIS_PASSWORD");
|
||||
$domain = getenv("NEXTCLOUD_DOMAIN");
|
||||
|
||||
$file = "/var/www/html/config/zz-docker-env.config.php";
|
||||
|
||||
$config = "<?php\n\n";
|
||||
|
||||
$config .= "\$CONFIG[\"dbhost\"] = "
|
||||
. var_export("mariadb", true)
|
||||
. ";\n";
|
||||
|
||||
$config .= "\$CONFIG[\"dbname\"] = "
|
||||
. var_export("nextcloud", true)
|
||||
. ";\n";
|
||||
|
||||
$config .= "\$CONFIG[\"dbuser\"] = "
|
||||
. var_export("nextcloud", true)
|
||||
. ";\n";
|
||||
|
||||
$config .= "\$CONFIG[\"dbpassword\"] = "
|
||||
. var_export($mysql, true)
|
||||
. ";\n\n";
|
||||
|
||||
$config .= "\$CONFIG[\"redis\"][\"host\"] = "
|
||||
. var_export("redis", true)
|
||||
. ";\n";
|
||||
|
||||
$config .= "\$CONFIG[\"redis\"][\"port\"] = 6379;\n";
|
||||
|
||||
$config .= "\$CONFIG[\"redis\"][\"password\"] = "
|
||||
. var_export($redis, true)
|
||||
. ";\n\n";
|
||||
|
||||
$config .= "\$CONFIG[\"overwriteprotocol\"] = "
|
||||
. var_export("https", true)
|
||||
. ";\n";
|
||||
|
||||
$config .= "\$CONFIG[\"overwrite.cli.url\"] = "
|
||||
. var_export("https://" . $domain, true)
|
||||
. ";\n\n";
|
||||
|
||||
$config .= "\$CONFIG[\"trusted_domains\"][] = "
|
||||
. var_export($domain, true)
|
||||
. ";\n";
|
||||
|
||||
$config .= "\$CONFIG[\"trusted_proxies\"][] = "
|
||||
. var_export("172.16.0.0/12", true)
|
||||
. ";\n";
|
||||
|
||||
file_put_contents($file, $config);
|
||||
'
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Permissions
|
||||
# --------------------------------------------------------
|
||||
|
||||
docker exec "$CONTAINER" \
|
||||
chown www-data:www-data "$OVERRIDE"
|
||||
|
||||
docker exec "$CONTAINER" \
|
||||
chmod 640 "$OVERRIDE"
|
||||
|
||||
echo "Configuration override written:"
|
||||
echo " $OVERRIDE"
|
||||
echo
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Show the file WITHOUT exposing passwords
|
||||
# --------------------------------------------------------
|
||||
|
||||
echo "Configuration written:"
|
||||
docker exec "$CONTAINER" \
|
||||
sed -E \
|
||||
's/(password"\] = ).*/\1"[HIDDEN]";/' \
|
||||
"$OVERRIDE"
|
||||
|
||||
echo
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Verify
|
||||
# --------------------------------------------------------
|
||||
|
||||
echo "=========================================="
|
||||
echo " Verification"
|
||||
echo "=========================================="
|
||||
echo
|
||||
|
||||
# Important:
|
||||
# config.php itself is not modified.
|
||||
# Nextcloud loads the additional config file.
|
||||
#
|
||||
# Re-read effective Nextcloud configuration.
|
||||
|
||||
EFFECTIVE_JSON="$(
|
||||
docker exec "$CONTAINER" php -r '
|
||||
$CONFIG = [];
|
||||
|
||||
include "/var/www/html/config/config.php";
|
||||
|
||||
if (file_exists("/var/www/html/config/zz-docker-env.config.php")) {
|
||||
include "/var/www/html/config/zz-docker-env.config.php";
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
"dbhost" => $CONFIG["dbhost"] ?? "",
|
||||
"dbname" => $CONFIG["dbname"] ?? "",
|
||||
"dbuser" => $CONFIG["dbuser"] ?? "",
|
||||
"dbpassword" => $CONFIG["dbpassword"] ?? "",
|
||||
|
||||
"redis_host" => $CONFIG["redis"]["host"] ?? "",
|
||||
"redis_password" => $CONFIG["redis"]["password"] ?? "",
|
||||
|
||||
"overwriteprotocol" =>
|
||||
$CONFIG["overwriteprotocol"] ?? "",
|
||||
|
||||
"overwrite_cli_url" =>
|
||||
$CONFIG["overwrite.cli.url"] ?? "",
|
||||
|
||||
"trusted_domains" =>
|
||||
$CONFIG["trusted_domains"] ?? [],
|
||||
|
||||
"trusted_proxies" =>
|
||||
$CONFIG["trusted_proxies"] ?? []
|
||||
]);
|
||||
'
|
||||
)"
|
||||
|
||||
EFFECTIVE_REDIS_PASSWORD="$(
|
||||
jq -r '.redis_password' <<< "$EFFECTIVE_JSON"
|
||||
)"
|
||||
|
||||
EFFECTIVE_DOMAINS="$(
|
||||
jq -r '.trusted_domains[]?' <<< "$EFFECTIVE_JSON"
|
||||
)"
|
||||
|
||||
if [[ "$EFFECTIVE_REDIS_PASSWORD" == "$REDIS_PASSWORD" ]]; then
|
||||
pass "Redis password fixed"
|
||||
else
|
||||
fail "Redis password was not fixed"
|
||||
fi
|
||||
|
||||
if grep -Fxq "$NEXTCLOUD_DOMAIN" <<< "$EFFECTIVE_DOMAINS"; then
|
||||
pass "trusted_domains fixed"
|
||||
else
|
||||
fail "trusted_domains was not fixed"
|
||||
fi
|
||||
|
||||
echo
|
||||
|
||||
echo "=========================================="
|
||||
echo " Fix complete"
|
||||
echo "=========================================="
|
||||
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# Summary
|
||||
# ============================================================
|
||||
|
||||
echo "=========================================="
|
||||
echo "Result"
|
||||
echo "=========================================="
|
||||
|
||||
echo "PASS: $PASS"
|
||||
echo "FAIL: $FAIL"
|
||||
|
||||
if [[ "$FAIL" -eq 0 ]]; then
|
||||
echo
|
||||
echo "PASS: configuration is consistent."
|
||||
exit 0
|
||||
else
|
||||
echo
|
||||
echo "FAIL: configuration has mismatches."
|
||||
echo
|
||||
echo "Run:"
|
||||
echo
|
||||
echo " $0 --fix"
|
||||
echo
|
||||
exit 1
|
||||
fi
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "==> Configuring Nextcloud"
|
||||
|
||||
OCC="php /var/www/html/occ"
|
||||
|
||||
# -------------------------
|
||||
# SMTP
|
||||
# -------------------------
|
||||
|
||||
$OCC config:system:set mail_smtpmode \
|
||||
--value="smtp"
|
||||
|
||||
$OCC config:system:set mail_smtphost \
|
||||
--value="${SMTP_HOST}"
|
||||
|
||||
$OCC config:system:set mail_smtpport \
|
||||
--value="${SMTP_PORT}"
|
||||
|
||||
$OCC config:system:set mail_smtpsecure \
|
||||
--value="${SMTP_SECURE}"
|
||||
|
||||
# Require SMTP authentication
|
||||
$OCC config:system:set mail_smtpauth \
|
||||
--type=boolean \
|
||||
--value="true"
|
||||
|
||||
$OCC config:system:set mail_smtpname \
|
||||
--value="${SMTP_USER}"
|
||||
|
||||
$OCC config:system:set mail_smtppassword \
|
||||
--value="${SMTP_PASSWORD}"
|
||||
|
||||
$OCC config:system:set mail_from_address \
|
||||
--value="${SMTP_FROM_ADDRESS}"
|
||||
|
||||
$OCC config:system:set mail_domain \
|
||||
--value="${SMTP_FROM_DOMAIN}"
|
||||
|
||||
|
||||
# -------------------------
|
||||
# Apps
|
||||
# -------------------------
|
||||
|
||||
apps=(
|
||||
calendar
|
||||
contacts
|
||||
tasks
|
||||
notes
|
||||
richdocuments
|
||||
cookbook
|
||||
)
|
||||
|
||||
for app in "${apps[@]}"; do
|
||||
echo "==> Installing app: ${app}"
|
||||
|
||||
if ! $OCC app:list | grep -q "^ ${app}:"; then
|
||||
$OCC app:install "${app}" || true
|
||||
fi
|
||||
|
||||
$OCC app:enable "${app}" || true
|
||||
done
|
||||
|
||||
|
||||
# -------------------------
|
||||
# Collabora
|
||||
# -------------------------
|
||||
|
||||
if [ -n "${COLLABORA_DOMAIN}" ]; then
|
||||
echo "==> Configuring Collabora"
|
||||
|
||||
$OCC config:app:set richdocuments wopi_url \
|
||||
--value="https://${COLLABORA_DOMAIN}"
|
||||
fi
|
||||
|
||||
|
||||
|
||||
echo "==> Creating users"
|
||||
export OC_PASS="${NEXTCLOUD_MIMI_PASSWORD}"
|
||||
# Mimi
|
||||
if ! $OCC user:info Mimi >/dev/null 2>&1; then
|
||||
$OCC user:add \
|
||||
--password-from-env \
|
||||
--display-name="Mimi" \
|
||||
Mimi
|
||||
fi
|
||||
|
||||
$OCC user:setting Mimi settings email "miriam.m.faber@gmail.com"
|
||||
|
||||
# Lucas
|
||||
if ! $OCC user:info lucas >/dev/null 2>&1; then
|
||||
echo "ERROR: User 'lucas' does not exist."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
$OCC user:setting lucas settings email "${LUCAS_EMAIL}"
|
||||
|
||||
echo "Set backroundjob to cron"
|
||||
$OCC background:cron
|
||||
|
||||
echo "==> Nextcloud post-installation configuration complete"
|
||||
@@ -0,0 +1,2 @@
|
||||
docker compose exec nextcloud php occ config:system:set overwriteprotocol --value=https
|
||||
docker compose exec nextcloud php occ config:system:set overwrite.cli.url --value=https://someoneslinux.ddns.net
|
||||
@@ -0,0 +1,304 @@
|
||||
# WebSocket support
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
# Redirect HTTP -> HTTPS
|
||||
server {
|
||||
listen 80;
|
||||
server_name
|
||||
cloud.someoneslinux.selfhost.eu
|
||||
media.someoneslinux.selfhost.eu
|
||||
audiobooks.someoneslinux.selfhost.eu
|
||||
login.someoneslinux.selfhost.eu
|
||||
auto.someoneslinux.selfhost.eu
|
||||
music.someoneslinux.selfhost.eu
|
||||
passwd.someoneslinux.selfhost.eu
|
||||
requests.someoneslinux.selfhost.eu
|
||||
torrent.someoneslinux.selfhost.eu
|
||||
status.someoneslinux.selfhost.eu;
|
||||
#ssl_conf_command Groups X25519:secp256r1:secp384r1;
|
||||
# ssl_conf_command Groups X25519:prime256v1:secp384r1;
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
# Common SSL settings
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
###################
|
||||
# NEXTCLOUD
|
||||
###################
|
||||
server {
|
||||
listen 443 ssl;http2 on;
|
||||
server_name cloud.someoneslinux.selfhost.eu;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" 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;
|
||||
|
||||
client_max_body_size 10G;
|
||||
|
||||
location / {
|
||||
proxy_pass http://nextcloud:80;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
|
||||
###################
|
||||
# JELLYFIN
|
||||
###################
|
||||
server {
|
||||
listen 443 ssl;http2 on;
|
||||
server_name media.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://jellyfin:8096;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
###################
|
||||
# AUDIOBOOKSHELF
|
||||
###################
|
||||
server {
|
||||
listen 443 ssl;http2 on;
|
||||
server_name audiobooks.someoneslinux.selfhost.eu;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" 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://audiobookshelf:80;
|
||||
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-Proto https;
|
||||
}
|
||||
}
|
||||
|
||||
###################
|
||||
# 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
|
||||
####################
|
||||
#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;
|
||||
# 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
|
||||
###################
|
||||
server {
|
||||
listen 443 ssl;http2 on;
|
||||
server_name music.someoneslinux.selfhost.eu;
|
||||
|
||||
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://navidrome:4533;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
|
||||
###################
|
||||
# VAULTWARDEN
|
||||
###################
|
||||
server {
|
||||
listen 443 ssl;http2 on;
|
||||
server_name passwd.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://vaultwarden:80;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
location /notifications/hub {
|
||||
proxy_pass http://vaultwarden:3012;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
}
|
||||
}
|
||||
|
||||
###################
|
||||
# SEERR
|
||||
###################
|
||||
server {
|
||||
listen 443 ssl;http2 on;
|
||||
server_name requests.someoneslinux.selfhost.eu;
|
||||
|
||||
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://seerr:5055;
|
||||
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-Proto https;
|
||||
}
|
||||
}
|
||||
|
||||
###################
|
||||
# QBITTORRENT
|
||||
###################
|
||||
server {
|
||||
listen 443 ssl;http2 on;
|
||||
server_name torrent.someoneslinux.selfhost.eu;
|
||||
|
||||
add_header Content-Security-Policy "frame-ancestors 'self';" always;
|
||||
# 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.2:8080;
|
||||
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-Proto https;
|
||||
}
|
||||
}
|
||||
|
||||
###################
|
||||
# GRFANA
|
||||
###################
|
||||
server {
|
||||
listen 443 ssl;http2 on;
|
||||
server_name status.someoneslinux.selfhost.eu;
|
||||
|
||||
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://grafana:3000;
|
||||
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-Proto https;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
services:
|
||||
nginx:
|
||||
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
|
||||
- ./html:/usr/share/nginx/html
|
||||
# Access and error logs
|
||||
- ./logs:/var/log/nginx
|
||||
networks:
|
||||
- shared-proxy-services
|
||||
|
||||
crowdsec:
|
||||
image: crowdsecurity/crowdsec:latest
|
||||
container_name: crowdsec
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
COLLECTIONS: >
|
||||
crowdsecurity/nginx
|
||||
crowdsecurity/http-cve
|
||||
volumes:
|
||||
- crowdsec-db:/var/lib/crowdsec/data
|
||||
- ./logs:/var/log/nginx:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
|
||||
|
||||
certbot:
|
||||
image: certbot/certbot:latest
|
||||
container_name: certbot
|
||||
volumes:
|
||||
- ./certs:/etc/letsencrypt
|
||||
- ./html:/var/www/certbot
|
||||
networks:
|
||||
- shared-proxy-services
|
||||
|
||||
networks:
|
||||
shared-proxy-services:
|
||||
external: true
|
||||
volumes:
|
||||
crowdsec-db:
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
# selfHOST DynDNS Update Script
|
||||
|
||||
URL='https://549335:GoviYPrfj4XI@carol.selfhost.de/nic/update?myip='
|
||||
|
||||
# Get current public IPv4 address
|
||||
PUBLIC_IP=$(curl -4 -s https://api.ipify.org)
|
||||
|
||||
# Exit if IP detection failed
|
||||
if [ -z "$PUBLIC_IP" ]; then
|
||||
echo "$(date): Failed to determine public IP"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Send update to selfHOST
|
||||
RESULT=$(curl -s "${URL}${PUBLIC_IP}")
|
||||
|
||||
echo "$(date): IP=${PUBLIC_IP}"
|
||||
@@ -0,0 +1,22 @@
|
||||
services:
|
||||
redis:
|
||||
image: redis:7
|
||||
container_name: redis
|
||||
restart: unless-stopped
|
||||
command: redis-server --appendonly yes --requirepass PerCent7heStorySp0tInOrd3r
|
||||
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"redis-cli -a \"$${REDIS_PASSWORD}\" ping | grep PONG"
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
|
||||
networks:
|
||||
default:
|
||||
external: true
|
||||
name: shared-redis-services
|
||||
@@ -0,0 +1,27 @@
|
||||
services:
|
||||
vaultwarden:
|
||||
image: vaultwarden/server:latest
|
||||
container_name: vaultwarden
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
WEBSOCKET_ENABLED: "true"
|
||||
SIGNUPS_ALLOWED: "true" # Registrierung deaktivieren (optional)
|
||||
DOMAIN: ${DOMAIN}
|
||||
ADMIN_TOKEN: ${ADMIN_TOKEN}
|
||||
SMTP_HOST: "smtp.web.de"
|
||||
SMTP_FROM: "ll_m@web.de"
|
||||
SMTP_PORT: 587
|
||||
SMTP_SSL: "false"
|
||||
SMTP_SECURITY: starttls
|
||||
SMTP_USERNAME: ll_m@web.de
|
||||
SMTP_PASSWORD: ${SMTP_PASSWORD}
|
||||
volumes:
|
||||
- ./vaultwarden:/data
|
||||
ports:
|
||||
- "8081:80"
|
||||
networks:
|
||||
- shared-proxy-services
|
||||
networks:
|
||||
shared-proxy-services:
|
||||
external: true
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
services:
|
||||
wireguard:
|
||||
image: lscr.io/linuxserver/wireguard:latest
|
||||
container_name: wireguard
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- SYS_MODULE
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Europe/Vienna
|
||||
- SERVERURL=someoneslinux.selfhost.eu
|
||||
- SERVERPORT=51820
|
||||
- PEERS=5
|
||||
volumes:
|
||||
- ./config:/config
|
||||
- /lib/modules:/lib/modules:ro
|
||||
ports:
|
||||
- 51820:51820/udp
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,34 @@
|
||||
services:
|
||||
diun:
|
||||
image: crazymax/diun:latest
|
||||
container_name: diun
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ./data:/data
|
||||
|
||||
environment:
|
||||
TZ: Europe/Vienna
|
||||
|
||||
# General
|
||||
DIUN_WATCH_WORKERS: 20
|
||||
DIUN_WATCH_SCHEDULE: "0 0 * * *"
|
||||
DIUN_PROVIDERS_DOCKER: "true"
|
||||
DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT: "true"
|
||||
|
||||
# Email notifications
|
||||
DIUN_NOTIF_MAIL_HOST: smtp.web.de
|
||||
DIUN_NOTIF_MAIL_PORT: 587
|
||||
DIUN_NOTIF_MAIL_USERNAME: ll_m@web.de
|
||||
DIUN_NOTIF_MAIL_PASSWORD: Mira2012
|
||||
DIUN_NOTIF_MAIL_FROM: ll_m@web.de
|
||||
DIUN_NOTIF_MAIL_TO: ll_m@web.de
|
||||
DIUN_NOTIF_MAIL_INSECURESKIPVERIFY: "false"
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
labels:
|
||||
- diun.enable=false
|
||||
@@ -0,0 +1,15 @@
|
||||
services:
|
||||
node-exporter:
|
||||
container_name: node-exporter
|
||||
image: prom/node-exporter:latest
|
||||
restart: unless-stopped
|
||||
pid: host
|
||||
command:
|
||||
- '--path.rootfs=/host'
|
||||
- '--collector.textfile.directory=/textfile'
|
||||
volumes:
|
||||
- '/:/host:ro,rslave'
|
||||
- ./node_exporter/textfile_collector:/textfile:ro
|
||||
ports:
|
||||
- '9100:9100'
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
|
||||
set -e
|
||||
|
||||
cp -f /hostscripts/torrent_mqtt.py /config/
|
||||
cp -f /hostscripts/torrent_qbittorrent_complete.sh /config/
|
||||
|
||||
chmod +x /config/torrent_qbittorrent_complete.sh
|
||||
|
||||
#if ! command -v scp >/dev/null 2>&1; then
|
||||
# echo "Install scp"
|
||||
#
|
||||
# for i in {1..12}; do
|
||||
# if apk add --no-cache openssh-client; then
|
||||
# break
|
||||
# fi
|
||||
#
|
||||
# echo "Retry $i/12"
|
||||
# sleep 5
|
||||
# done
|
||||
#fi
|
||||
|
||||
if [ ! -d /config/.venv ]; then
|
||||
echo "Generate venv"
|
||||
python3 -m venv /config/.venv
|
||||
/config/.venv/bin/pip install --upgrade pip
|
||||
/config/.venv/bin/pip install paho-mqtt
|
||||
fi
|
||||
|
||||
chmod 700 /config/.ssh
|
||||
chmod 600 /config/.ssh/id_ed25519
|
||||
chmod 644 /config/.ssh/id_ed25519.pub
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
#!/bin/bash
|
||||
|
||||
###############################################################################
|
||||
# Description:
|
||||
# qBittorrent external script that:
|
||||
# 1. Receives torrent name, root path and category from qBittorrent.
|
||||
# 2. Sends the torrent name to torrent_mqtt.py.
|
||||
# 3. Copies the torrent contents to mediahost via SCP.
|
||||
# 4. Logs all output to a timestamped logfile.
|
||||
#
|
||||
# Usage:
|
||||
# torrent_complete.sh <torrent_name> <root_path> <category>
|
||||
#
|
||||
# qBittorrent configuration:
|
||||
# /home/lucas/scripts/serverhost/torrent_complete.sh "%N" "%R" "%L"
|
||||
#
|
||||
# Arguments:
|
||||
# $1 Torrent name
|
||||
# $2 Root path
|
||||
# $3 Torrent category
|
||||
#
|
||||
# Log files:
|
||||
# /home/lucas/.LOG/log_torrent_name_YYYY-MM-DD_HH-MM-SS.log
|
||||
#
|
||||
# Requirements:
|
||||
# - bash
|
||||
# - scp
|
||||
# - SSH access to "mediahost"
|
||||
# - /home/lucas/scripts/serverhost/torrent_mqtt.py
|
||||
###############################################################################
|
||||
|
||||
set -u
|
||||
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
|
||||
###############################################################################
|
||||
# Usage
|
||||
###############################################################################
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage:
|
||||
$SCRIPT_NAME <torrent_name> <root_path> <category>
|
||||
|
||||
Description:
|
||||
Sends the torrent name to torrent_mqtt.py and copies the torrent
|
||||
contents to mediahost.
|
||||
|
||||
Arguments:
|
||||
torrent_name Torrent name
|
||||
root_path Torrent root path
|
||||
category Torrent category
|
||||
|
||||
Example:
|
||||
$SCRIPT_NAME "Ubuntu 24.04" "/downloads/Ubuntu 24.04" "Linux"
|
||||
|
||||
qBittorrent:
|
||||
$SCRIPT_NAME "%N" "%R" "%L"
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Check arguments
|
||||
###############################################################################
|
||||
|
||||
if [[ $# -ne 3 ]]; then
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TORRENT_NAME="$1"
|
||||
ROOT_PATH="$2"
|
||||
CATEGORY="$3"
|
||||
|
||||
if [[ "$CATEGORY" != *arr* ]]; then
|
||||
###############################################################################
|
||||
# Start
|
||||
###############################################################################
|
||||
|
||||
echo "=================================================="
|
||||
echo "Torrent transfer started"
|
||||
echo "Date: $(date)"
|
||||
echo "Script: $SCRIPT_NAME"
|
||||
echo "Torrent name: $TORRENT_NAME"
|
||||
echo "Root path: $ROOT_PATH"
|
||||
echo "Category: $CATEGORY"
|
||||
echo "=================================================="
|
||||
|
||||
###############################################################################
|
||||
# Check for scp command
|
||||
###############################################################################
|
||||
|
||||
if ! command -v scp >/dev/null 2>&1; then
|
||||
echo "Install scp"
|
||||
sed -i 's|http://|https://|g' /etc/apk/repositories
|
||||
for i in {1..12}; do
|
||||
if apk add --no-cache openssh-client; then
|
||||
break
|
||||
fi
|
||||
|
||||
echo "Retry $i/12"
|
||||
sleep 5
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Send torrent name to MQTT script
|
||||
###############################################################################
|
||||
|
||||
echo
|
||||
echo "--- Sending torrent name to torrent_mqtt.py ---"
|
||||
|
||||
/config/.venv/bin/python3 /config/torrent_mqtt.py "$TORRENT_NAME"
|
||||
|
||||
MQTT_STATUS=$?
|
||||
|
||||
echo "torrent_mqtt.py exit status: $MQTT_STATUS"
|
||||
|
||||
###############################################################################
|
||||
# Check root path
|
||||
###############################################################################
|
||||
|
||||
echo
|
||||
echo "--- Checking root path ---"
|
||||
|
||||
if [[ ! -d "$ROOT_PATH" ]]; then
|
||||
echo "ERROR: Root path does not exist or is not a directory:"
|
||||
echo " $ROOT_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Root path exists."
|
||||
|
||||
###############################################################################
|
||||
# Copy torrent to mediahost
|
||||
###############################################################################
|
||||
|
||||
basename_root=$(basename "$ROOT_PATH")
|
||||
echo $basename_root
|
||||
if [ "$basename_root" = "downloads" ]; then
|
||||
remote_dir="$TORRENT_NAME"
|
||||
else
|
||||
remote_dir="$basename_root"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "--- Copying torrent to mediahost ---"
|
||||
echo "Source: $ROOT_PATH"
|
||||
echo "Destination: mediahost:/torrent/qbittorrent/mediahost/"
|
||||
|
||||
ssh -i /config/.ssh/id_ed25519 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
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/
|
||||
SCP_STATUS=$?
|
||||
|
||||
echo "scp exit status: $SCP_STATUS"
|
||||
|
||||
###############################################################################
|
||||
# Result
|
||||
###############################################################################
|
||||
|
||||
echo
|
||||
echo "=================================================="
|
||||
|
||||
if [[ $MQTT_STATUS -ne 0 ]]; then
|
||||
echo "WARNING: torrent_mqtt.py failed."
|
||||
fi
|
||||
if [[ $SSH_STATUS -eq 0 ]]; then
|
||||
echo "Target Dir generated"
|
||||
else
|
||||
echo "ERROR: Target dir generation failed."
|
||||
fi
|
||||
|
||||
if [[ $SCP_STATUS -eq 0 ]]; then
|
||||
echo "Torrent transfer completed successfully."
|
||||
else
|
||||
echo "ERROR: Torrent transfer failed."
|
||||
fi
|
||||
|
||||
echo "Finished: $(date +'%y%m%d %H%M%Sr')"
|
||||
echo "=================================================="
|
||||
else
|
||||
echo "ARR handled torrent: $(date +'%y%m%d %H%M%S'): $TORRENT_NAME"
|
||||
fi
|
||||
|
||||
# Return SCP's exit status to qBittorrent
|
||||
exit 0
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
services:
|
||||
qbittorrent:
|
||||
#image: "lscr.io/linuxserver/qbittorrent:latest"
|
||||
build: .
|
||||
container_name: "qbittorrent"
|
||||
environment:
|
||||
- PGID=1000
|
||||
- TZ=Europe/Vienna
|
||||
- PUID=1000
|
||||
- WEBUI_PORT=8080
|
||||
ports:
|
||||
- "8080:8080/tcp"
|
||||
- "55661:55661/udp"
|
||||
- "55661:55661/tcp"
|
||||
restart: "unless-stopped"
|
||||
|
||||
volumes:
|
||||
- "./10-torrent-init.sh:/custom-cont-init.d/10-torrent-init.sh:ro"
|
||||
- "./config:/config"
|
||||
- "/home/lucas/scripts/torrent:/hostscripts:ro"
|
||||
- "/torrent/qbittorrent/downloads:/downloads"
|
||||
- "/torrent/qbittorrent/torrents:/torrents"
|
||||
- "/torrent/qbittorrent/watch:/watch"
|
||||
Executable
+1375
File diff suppressed because it is too large.
Load diff
Executable
+183
@@ -0,0 +1,183 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
usage(){
|
||||
printf "Usage:
|
||||
-i <file>\tInfo
|
||||
-av <file>\tChange Audio and Video Codec
|
||||
-v <file>\tChange Video Codec
|
||||
-a <file>\tChange Audio Codec
|
||||
-s <file>\tRemove Subtitles
|
||||
-rm <file>\tRemove First Audio
|
||||
-subs <file>\tEmbed subtitles
|
||||
-dir_down\tDownsize all files in dir to 1920x1080
|
||||
-find\tFind files from file and resize
|
||||
-d <file>\tDownsize input video
|
||||
-filter <file> filter video file\n"
|
||||
}
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "Too few arguments <file> <part of filename to remove>"
|
||||
else
|
||||
if [ -d "$2" ] || [ ]; then
|
||||
echo "Argument is directory"
|
||||
input_dir=$2
|
||||
elif [ -f "$2" ]; then
|
||||
echo "Argument is file"
|
||||
input=$2
|
||||
else
|
||||
echo "Argument $2 doesnt exist"
|
||||
exit
|
||||
fi
|
||||
if [ "$1" == "-av" ]; then
|
||||
ffmpeg -i "$input" -vcodec mpeg4 -acodec mp3 -preset veryslow -qp 0 "_$input"
|
||||
elif [ "$1" == "-s" ]; then
|
||||
ffmpeg -i "$input" -sn -c copy "_$input"
|
||||
elif [ "$1" == "-a" ]; then
|
||||
ffmpeg -i "$input" -vcodec copy -acodec mp3 -preset veryslow -qp 0 "_$input"
|
||||
elif [ "$1" == "-v" ]; then
|
||||
ffmpeg -i "$input" -vcodec mpeg4 -preset veryslow -qp 0 -acodec copy "_$input"
|
||||
elif [ "$1" == "-rm" ]; then
|
||||
ffmpeg -i "$input" -map 0 -map -0:a:0 -c copy -y "_$input";
|
||||
# mv "_$file" "$file";
|
||||
echo $input
|
||||
elif [ "$1" == "-i" ]; then
|
||||
ffmpeg -i "$input"
|
||||
elif [ "$1" == "-d" ]; then
|
||||
ffmpeg -i "$input" -c:v libx264 -crf 27 "${input%%.*}_downsize.${input##*.}"
|
||||
elif [ "$1" == "-subs" ]; then
|
||||
#for file in *${input}; do ls "subs/$(echo $file | rev | cut -f2- -d'.' | rev).srt" ; done
|
||||
for file in *${input}; do
|
||||
ls "$(echo $file | rev | cut -f2- -d'.' | rev).srt"
|
||||
ffmpeg -i "$file" -f srt -i "$(echo $file | rev | cut -f2- -d'.' | rev).srt" -c copy -c:s mov_text "_$file"
|
||||
mv "_$file" "$file"
|
||||
done
|
||||
elif [ "$1" == "-frame" ]; then
|
||||
ffmpeg -ss 00:00:05.01 -i myvideo.avi -frames:v 1 myimage.jpg
|
||||
elif [ "$1" == "-cut" ]; then
|
||||
ffmpeg -ss 00:00:02 -i GMT20210112-071517_Vorlesung-_1920x1220.mp4 -to 01:28:49 -c copy output.mp4
|
||||
elif [ "$1" == "-dir-down" ]; then
|
||||
echo "Resize all files in ${input_dir}"
|
||||
if [ ! -d "${input_dir}/comp" ];then
|
||||
mkdir "${input_dir}/comp"
|
||||
fi
|
||||
for file in ${input_dir}/*; do
|
||||
if [ -f "${file}" ]; then
|
||||
#echo ${file%.*} ${file##*.} ${file##*/}
|
||||
ffmpeg -i "${file}" -vf scale=1920x1080 "${input_dir}/comp/${file##*/}" 2>/dev/null 1>&2
|
||||
fi
|
||||
done
|
||||
elif [ "$1" == "-find" ]; then
|
||||
echo "Find files in ${input_dir}"
|
||||
if [ ! -d "${input_dir}/comp" ];then
|
||||
mkdir ${input_dir}/comp
|
||||
fi
|
||||
while read -r line; do
|
||||
file=$(find . -name "$line")
|
||||
if [ -f "${file}" ]; then
|
||||
#echo ${file%.*}
|
||||
#echo ${file%/*}
|
||||
#echo ${file##*.}
|
||||
|
||||
ffmpeg -i "${file}" -vf scale=1920x1080 "${input_dir}/comp/${file##*/}"
|
||||
fi
|
||||
done<$3
|
||||
echo "Converted all files in ${input_dir}"
|
||||
elif [ "$1" == "-file2mp3" ]; then
|
||||
codec=$3
|
||||
find "${input_dir}" -name "*$codec" -print0 | while read -d $'\0' file; do
|
||||
#echo $file
|
||||
echo ${file%.*}
|
||||
|
||||
#ffmpeg -i "$file" -sample_fmt s16 -ar 48000 "${file%.*}_.flac"< /dev/null
|
||||
#mv "${file%.*}_.flac" "$file"
|
||||
ffmpeg -i "$file" -vn -ar 44100 -ac 2 -b:a 192k "${file%.*}.mp3"< /dev/null
|
||||
#ffmpeg -i "$file" -ab 320k -map_metadata 0 -id3v2_version 3 "${file%.*}.mp3"< /dev/null
|
||||
done
|
||||
elif [ "$1" == "-filter" ]; then
|
||||
#!/usr/bin/env bash
|
||||
|
||||
|
||||
|
||||
file="$2"
|
||||
selected_stream="$3"
|
||||
file_new="${file%.*}_processed.${file##*.}"
|
||||
|
||||
if [[ -z "$file" ]]; then
|
||||
echo "Usage: $0 input_file [audio_stream_index]"
|
||||
exit 1
|
||||
fi
|
||||
# Get list of audio streams
|
||||
mapfile -t streams < <(
|
||||
ffprobe -v error -select_streams a \
|
||||
-show_entries stream=index:stream_tags=language \
|
||||
-of json "$file" |
|
||||
jq -r '.streams[] | "\(.index),\(.tags.language // "unknown")"'
|
||||
)
|
||||
|
||||
stream_count=${#streams[@]}
|
||||
|
||||
if [[ "$stream_count" -eq 0 ]]; then
|
||||
echo "No audio streams found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Found $stream_count audio stream(s)."
|
||||
|
||||
# If not provided, prompt when multiple exist
|
||||
if [[ -z "$selected_stream" ]]; then
|
||||
if [[ "$stream_count" -eq 1 ]]; then
|
||||
selected_stream="${streams[0]%,*}"
|
||||
echo "Using only audio stream: $selected_stream"
|
||||
else
|
||||
echo "Available audio streams:"
|
||||
i=0
|
||||
for s in "${streams[@]}"; do
|
||||
idx="${s%,*}"
|
||||
lang="${s#*,}"
|
||||
echo "[$i] stream_index=$idx language=${lang:-unknown}"
|
||||
((i++))
|
||||
done
|
||||
|
||||
read -rp "Select audio stream index (ffmpeg stream index): " selected_stream
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Processing audio stream: $selected_stream"
|
||||
|
||||
ffmpeg -i "$file" \
|
||||
-map 0:v -c:v copy \
|
||||
-map 0:a:"$selected_stream" \
|
||||
-filter_complex "[0:a]highpass=f=100,\
|
||||
acompressor=threshold=-22dB:ratio=4:attack=10:release=150,\
|
||||
equalizer=f=2500:t=q:w=1:g=5,\
|
||||
equalizer=f=4000:t=q:w=1:g=4,\
|
||||
deesser=i=0.4:f=0.25,\
|
||||
alimiter=limit=0.95,\
|
||||
dynaudnorm=f=200:g=15[a]" \
|
||||
-map "[a]" -c:a libvorbis \
|
||||
"$file_new"
|
||||
|
||||
echo "Done: $file_new"
|
||||
else
|
||||
usage
|
||||
exit
|
||||
fi
|
||||
fi
|
||||
#Uncompatible Videocodecs:
|
||||
# mpeg4 (DX50 / 0x30355844)
|
||||
#h264 (High) (avc1 / 0x31637661)
|
||||
#Uncompatible AUdiocodecs:
|
||||
|
||||
#Multiple Audiolines
|
||||
|
||||
#Subtitles
|
||||
# ffmpeg -i $input -sn -vcodec copy -acodec copy
|
||||
#Keep Audio/Video Settings
|
||||
# ffmpeg -i $input -acodec copy -vcodec copy
|
||||
#delete audio trach 0:1
|
||||
#for file in *avi ; do ffmpeg -i "$file" -map 0:0 -map 0:2 -acodec copy -vcodec copy "New_$file"; mv "New_$file" "$file"; done
|
||||
#ffmpeg -ss 00:00:00 -i Stargate.mp4 -to 02:09:39 -c copy output.mp4
|
||||
# Embed Subtitles
|
||||
#for file in Subs/.*srt; do ffmpeg -i "$file" "$(echo $file | sed 's/srt/ass/g')"; done
|
||||
#for file in *avi; do ffmpeg -i "$file" -vf ass="Subs/$(echo $file | sed 's/avi/ass/g' | sed 's/S01E/s01e/g')" "New1_$file" ;done
|
||||
|
||||
Executable
+293
@@ -0,0 +1,293 @@
|
||||
#!/home/lucas/scripts/.venv/bin/python3
|
||||
import os
|
||||
import re
|
||||
import random
|
||||
import datetime
|
||||
import sys
|
||||
from PIL import Image, ExifTags
|
||||
from pillow_heif import open_heif
|
||||
import subprocess
|
||||
import json
|
||||
from serverlibary import safe_rename
|
||||
from pathlib import Path
|
||||
# Folder to scan
|
||||
folder_path = '.'
|
||||
# Supported image and video extensions
|
||||
|
||||
video_extensions = {
|
||||
'.mp4', '.mov', '.avi', '.mkv', '.hevc', '.mov',
|
||||
}
|
||||
img_extensions = {
|
||||
'.jpg', '.jpeg', '.png', '.heic', '.webp',
|
||||
}
|
||||
|
||||
extensions = img_extensions | video_extensions
|
||||
prefixes = { 'IMG', 'VID'}
|
||||
# Build reusable groups for regex
|
||||
ext_group = r'(jpg|jpeg|png|heic|webp|mp4|mov|avi|mkv|hevc)'
|
||||
prefix_group = r'(?:IMG|VID)'
|
||||
# Correct format to keep
|
||||
correct_format_re = re.compile(
|
||||
rf'^{prefix_group}_(\d{{8}}_\d{{6}}).*\.{ext_group}$',
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
||||
# Alternate timestamp formats and their patterns
|
||||
alt_patterns = [
|
||||
# PREFIX_YYYY_MM_DD_HH_MM_SS.ext
|
||||
(
|
||||
re.compile(
|
||||
rf'^(?:{prefix_group}_)?(\d{{4}})_(\d{{2}})_(\d{{2}})_(\d{{2}})_(\d{{2}})_(\d{{2}}).*\.{ext_group}$',
|
||||
re.IGNORECASE
|
||||
),
|
||||
'%Y_%m_%d_%H_%M_%S'
|
||||
),
|
||||
|
||||
# PREFIX_YYYYMMDD_HHMMSS.ext
|
||||
(
|
||||
re.compile(
|
||||
rf'^(?:{prefix_group}_)?(\d{{8}})_(\d{{6}}).*\.{ext_group}$',
|
||||
re.IGNORECASE
|
||||
),
|
||||
'%Y%m%d_%H%M%S'
|
||||
),
|
||||
|
||||
# PREFIX_YYYY.MM.DD-HHMMSS.ext
|
||||
(
|
||||
re.compile(
|
||||
rf'^(?:{prefix_group}_)?(\d{{4}})\.(\d{{2}})\.(\d{{2}})-(\d{{6}}).*\.{ext_group}$',
|
||||
re.IGNORECASE
|
||||
),
|
||||
'%Y.%m.%d-%H%M%S'
|
||||
),
|
||||
|
||||
# PREFIX_YYYYMMDD.ext
|
||||
(
|
||||
re.compile(
|
||||
rf'^(?:{prefix_group}_)?(\d{{8}})\.{ext_group}$',
|
||||
re.IGNORECASE
|
||||
),
|
||||
'%Y%m%d'
|
||||
),
|
||||
|
||||
# PREFIX_HHMMSS.ext
|
||||
(
|
||||
re.compile(
|
||||
rf'^(?:{prefix_group}_)?(\d{{6}})\.{ext_group}$',
|
||||
re.IGNORECASE
|
||||
),
|
||||
'%H%M%S'
|
||||
),
|
||||
]
|
||||
|
||||
# EXIF tag name for datetime
|
||||
EXIF_DATE_TAGS = ['DateTimeOriginal', 'DateTimeDigitized', 'DateTime']
|
||||
|
||||
|
||||
def local_safe_rename(src_path, new_filename):
|
||||
"""Rename file safely, avoiding overwrites"""
|
||||
directory = os.path.dirname(src_path)
|
||||
base, ext = os.path.splitext(new_filename)
|
||||
counter = 1
|
||||
final_name = new_filename
|
||||
|
||||
while os.path.exists(os.path.join(directory, final_name)):
|
||||
final_name = f"{base}_{counter}{ext}"
|
||||
counter += 1
|
||||
|
||||
os.rename(src_path, os.path.join(directory, final_name))
|
||||
print(f"Renamed: {os.path.basename(src_path)} ➜ {final_name}")
|
||||
|
||||
def get_exif_creation_datetime(filepath):
|
||||
"""Try to extract creation datetime from image EXIF metadata"""
|
||||
try:
|
||||
if filepath.lower().endswith((".mp4", ".mov", ".m4v")):
|
||||
# WhatsApp videos: use filesystem modification time
|
||||
if "WA" in os.path.basename(filepath):
|
||||
return datetime.datetime.fromtimestamp(
|
||||
os.path.getmtime(filepath)
|
||||
)
|
||||
|
||||
tags = ["CreateDate", "MediaCreateDate", "TrackCreateDate"]
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
"exiftool",
|
||||
"-j",
|
||||
"-CreateDate",
|
||||
"-MediaCreateDate",
|
||||
"-TrackCreateDate",
|
||||
filepath,
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
metadata = json.loads(result.stdout)[0]
|
||||
|
||||
for tag in tags:
|
||||
if metadata.get(tag):
|
||||
return datetime.datetime.strptime(
|
||||
metadata[tag],
|
||||
"%Y:%m:%d %H:%M:%S"
|
||||
)
|
||||
|
||||
# Fallback: use filesystem modification time
|
||||
return datetime.datetime.fromtimestamp(
|
||||
os.path.getmtime(filepath)
|
||||
)
|
||||
|
||||
else:
|
||||
with Image.open(filepath) as img:
|
||||
exif = img._getexif()
|
||||
if not exif:
|
||||
print("Exif failed")
|
||||
return None
|
||||
|
||||
tag_label = {
|
||||
ExifTags.TAGS.get(k): v
|
||||
for k, v in exif.items()
|
||||
if k in ExifTags.TAGS
|
||||
}
|
||||
|
||||
for tag in EXIF_DATE_TAGS:
|
||||
print(tag)
|
||||
if tag in tag_label:
|
||||
print(tag)
|
||||
return datetime.datetime.strptime(
|
||||
tag_label[tag],
|
||||
'%Y:%m:%d %H:%M:%S'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Exif Error {e}")
|
||||
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def get_manufacturer(file_path):
|
||||
# Call exiftool and get JSON output
|
||||
result = subprocess.run(
|
||||
["exiftool", "-j", file_path], # -j outputs JSON
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
# Parse JSON output
|
||||
metadata = json.loads(result.stdout)[0]
|
||||
# Extract Author tag and remove spaces
|
||||
author = metadata.get("Author", "")
|
||||
if author == "":
|
||||
author = metadata.get("AndroidMake", "")
|
||||
if author == "":
|
||||
author = metadata.get("AndroidManufacturer", "")
|
||||
if author == "":
|
||||
author = metadata.get("Model", "")
|
||||
if author == "":
|
||||
author = "NoAuthor"
|
||||
author_no_spaces = author.replace(" ", "")
|
||||
return author_no_spaces
|
||||
|
||||
def process_images(folder):
|
||||
folder = Path(folder)
|
||||
for fname in os.listdir(folder):
|
||||
fpath = os.path.join(folder, fname)
|
||||
|
||||
if not os.path.isfile(fpath):
|
||||
continue
|
||||
|
||||
_, ext = os.path.splitext(fname)
|
||||
ext = ext.lower()
|
||||
|
||||
if ext not in extensions:
|
||||
continue
|
||||
|
||||
# Step 1: Normalize filename by replacing '-' with '_'
|
||||
normalized_name = fname.replace('-', '_')
|
||||
|
||||
if normalized_name != fname:
|
||||
# Rename the file to the normalized name before continuing
|
||||
new_path = os.path.join(folder, normalized_name)
|
||||
|
||||
safe_rename(fpath, new_path)
|
||||
|
||||
print(f"Normalized: {fname} ➜ {os.path.basename(new_path)}")
|
||||
|
||||
# Update fname and fpath to the new normalized filename
|
||||
fname = os.path.basename(new_path)
|
||||
fpath = new_path
|
||||
|
||||
videoManufacturer = ""
|
||||
file_prefix = "IMG"
|
||||
for extention in video_extensions:
|
||||
if extention in ext:
|
||||
file_prefix = "VID"
|
||||
videoManufacturer = "_" + get_manufacturer(fpath)
|
||||
for extention in img_extensions:
|
||||
if extention in ext:
|
||||
file_prefix = "IMG"
|
||||
|
||||
# Now continue with your existing logic on normalized fname
|
||||
if correct_format_re.match(fname):
|
||||
if videoManufacturer != "" and file_prefix == "VID":
|
||||
if videoManufacturer not in fname:
|
||||
basename, _ = os.path.splitext(fname)
|
||||
new_fname = f"{basename}{videoManufacturer}{ext}"
|
||||
safe_rename(Path(fpath),folder / new_fname)
|
||||
continue
|
||||
|
||||
print(f"[OK] {fname} already correctly named.")
|
||||
continue
|
||||
|
||||
matched = False
|
||||
for pattern, fmt in alt_patterns:
|
||||
m = pattern.match(normalized_name)
|
||||
if m:
|
||||
try:
|
||||
parts = m.groups()
|
||||
timestamp_str = '_'.join(parts[:2]) if len(parts) >= 2 else parts[0]
|
||||
dt = datetime.datetime.strptime(timestamp_str, fmt)
|
||||
new_fname = f"{file_prefix}_{dt.strftime('%Y%m%d_%H%M%S')}{videoManufacturer}{ext}"
|
||||
safe_rename(Path(fpath),folder / new_fname)
|
||||
matched = True
|
||||
break
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
if matched:
|
||||
continue
|
||||
|
||||
# 3. Try reading creation date from metadata
|
||||
dt = get_exif_creation_datetime(fpath)
|
||||
if dt:
|
||||
new_fname = f"{file_prefix}_{dt.strftime('%Y%m%d_%H%M%S')}{videoManufacturer}{ext}"
|
||||
safe_rename(Path(fpath),folder / new_fname)
|
||||
continue
|
||||
else:
|
||||
print(f"[Fail] Creation Date for {fpath} not found")
|
||||
# 4. Fallback: keep original name, add _RX_<random> (only if not already added)
|
||||
if '_RX_' in fname:
|
||||
print(f"[SKIP] {fname} already has fallback name.")
|
||||
continue
|
||||
|
||||
basename, _ = os.path.splitext(fname)
|
||||
rand_num = random.randint(100000, 999999)
|
||||
new_fname = f"{basename}_RX_{rand_num}{videoManufacturer}{ext}"
|
||||
safe_rename(Path(fpath),folder / new_fname)
|
||||
|
||||
# ---- Main script entry ----
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python script.py <image_folder_path>")
|
||||
sys.exit(1)
|
||||
|
||||
folder_path = sys.argv[1]
|
||||
|
||||
if not os.path.isdir(folder_path):
|
||||
print(f"Error: '{folder_path}' is not a valid directory.")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Processing images in: {os.path.abspath(folder_path)}\n")
|
||||
process_images(folder_path)
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
FOLDER=/data/nextcloud/syncuser/pictures/Camera/
|
||||
|
||||
export PYTHONPATH=/home/lucas/scripts
|
||||
syncNC -c
|
||||
/home/lucas/scripts/file\ processing/rename_camera_folder.py ${FOLDER}
|
||||
|
||||
syncNC -c
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
FOLDER=/data/nextcloud/syncuser/Memes
|
||||
|
||||
syncNC -m
|
||||
for IMG_ORIG in ${FOLDER}/*; do
|
||||
if [[ "$(echo ${IMG_ORIG##*/} | grep "-")" != "" ]] || [[ "$(echo ${IMG_ORIG##*/} | grep " ")" != "" ]] ;then
|
||||
mv "${IMG_ORIG}" "$(echo ${IMG_ORIG} | sed 's/-/_/g' | sed 's/ /_/g')"
|
||||
IMG_ORIG="$(echo ${IMG_ORIG} | sed 's/-/_/g' | sed 's/ /_/g')"
|
||||
fi
|
||||
if [[ -f ${IMG_ORIG} ]] ; then
|
||||
IMG_EXT=${IMG_ORIG##*/}
|
||||
IMG=${IMG_EXT%%.*}
|
||||
EXT=${IMG_ORIG##*.}
|
||||
DIR=${IMG_ORIG%/*}
|
||||
if [ "$(echo ${IMG} | grep "MEMES")" == "" ]; then
|
||||
LAST_IDX=$(ls ${FOLDER}/MEMES* | rev | cut -f1 -d'/' | rev | cut -f1 -d'.' | sort | tail -1 | sed 's/MEMES//g')
|
||||
NEW_IDX=$(echo "${LAST_IDX}+1" | bc)
|
||||
mv ${IMG_ORIG} "$(printf "%s/MEMES%04d.%s" ${DIR} ${NEW_IDX} ${EXT})"
|
||||
IMG_ORIG="$(printf "%s/MEMES%04d.%s" ${DIR} ${NEW_IDX} ${EXT})"
|
||||
IMG_EXT=${IMG_ORIG##*/}
|
||||
IMG=${IMG_EXT%%.*}
|
||||
EXT=${IMG_ORIG##*.}
|
||||
fi
|
||||
if [ "$(echo ${EXT} | grep "webp")" != "" ]; then
|
||||
echo Pic: $IMG
|
||||
convert ${IMG_ORIG} $(echo ${IMG_ORIG} | sed 's/webp/jpg/g')
|
||||
rm ${IMG_ORIG}
|
||||
elif [ "$(echo ${EXT} | grep "png")" != "" ]; then
|
||||
echo Pic: $IMG
|
||||
convert ${IMG_ORIG} $(echo ${IMG_ORIG} | sed 's/png/jpg/g')
|
||||
rm ${IMG_ORIG}
|
||||
elif [ "$(echo ${EXT} | grep "webm")" != "" ]; then
|
||||
echo Vid: $IMG
|
||||
ffmpeg -y -i ${IMG_ORIG} $(echo ${IMG_ORIG} | sed 's/webm/mp4/g')
|
||||
rm ${IMG_ORIG}
|
||||
fi
|
||||
fi
|
||||
done
|
||||
syncNC -m
|
||||
echo Fin
|
||||
@@ -0,0 +1 @@
|
||||
find . -type f -name '*conflict**' -exec sh -c 'x="{}"; mv "$x" "$(echo "$x" | sed "s/ (conflicted copy 2025-01-06 190022)//g")" ' \;
|
||||
Executable
+98
@@ -0,0 +1,98 @@
|
||||
#!/bin/bash
|
||||
|
||||
commit1=$1
|
||||
commit2=$2
|
||||
dir=$3
|
||||
patches_dir=$4
|
||||
CHECK=1
|
||||
APPLY=1
|
||||
|
||||
if [ -z "$commit1" ] || [ -z "$commit2" ] || [ -z "$dir" ]; then
|
||||
echo "Usage: $0 <commit1> <commit2> <directory> [patches_dir]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Default patches directory if not provided
|
||||
if [ -z "$patches_dir" ]; then
|
||||
patches_dir="$(pwd)/patches"
|
||||
elif [[ "$patches_dir" != /* ]]; then
|
||||
patches_dir="$(pwd)/${patches_dir}patches"
|
||||
fi
|
||||
|
||||
if [[ "$commit2" == "HEAD" ]]; then
|
||||
commit2=""
|
||||
fi
|
||||
|
||||
echo "Diffing $dir from $commit1 to ${commit2:-HEAD}..."
|
||||
echo "Patches will be saved in: $patches_dir"
|
||||
|
||||
|
||||
# Save current directory
|
||||
orig_dir="$(pwd)"
|
||||
|
||||
# Get git root directory relative to $dir
|
||||
git_root=$(git -C "$dir" rev-parse --show-toplevel) || { echo "Not a git repo: $dir"; exit 1; }
|
||||
|
||||
# Compute relative path of $dir from git root
|
||||
relative_dir=$(realpath --relative-to="$git_root" "$dir")
|
||||
echo "Relative path of $dir from git root: $relative_dir"
|
||||
|
||||
# Change to git root directory
|
||||
cd "$git_root" || { echo "Failed to cd to git root of $dir"; exit 1; }
|
||||
echo "Go to $git_root"
|
||||
mkdir -p "$patches_dir"
|
||||
|
||||
# Load list of changed files into an array
|
||||
mapfile -t diff_files < <(git diff --name-only $commit1..$commit -- $relative_dir )
|
||||
|
||||
|
||||
for file in "${diff_files[@]}"; do
|
||||
git diff $commit1..$commit2 --stat -- "$file"
|
||||
done
|
||||
|
||||
for file in "${diff_files[@]}"; do
|
||||
echo $file
|
||||
git diff $commit1..$commit2 -- "$file" > "$patches_dir/$(basename "$file").patch"
|
||||
done
|
||||
for patch in "$patches_dir"/*.patch; do
|
||||
echo $patch
|
||||
for patch in "$patches_dir"/*.patch; do
|
||||
sed -i 's|trixel3_software/||g' "$patch"
|
||||
done
|
||||
done
|
||||
|
||||
# Get git root directory relative to $dir
|
||||
git_root_patches_dir=$(git -C "$patches_dir" rev-parse --show-toplevel) || { echo "Not a git repo: $patches_dir"; exit 1; }
|
||||
|
||||
# Compute relative path of $dir from git root
|
||||
relative_dir_patches_dir=$(realpath --relative-to="$git_root_patches_dir" "$patches_dir")
|
||||
echo "Relative path of $patches_dir from git root: $relative_dir_patches_dir"
|
||||
|
||||
# Change to git root directory
|
||||
cd "$git_root_patches_dir" || { echo "Failed to cd to git root of $patches_dir"; exit 1; }
|
||||
echo "Go to $git_root_patches_dir"
|
||||
|
||||
if [[ $CHECK -eq 1 ]]; then
|
||||
# check patches
|
||||
echo "CHECK Patches"
|
||||
for patch in "$patches_dir"/*.patch; do
|
||||
echo $patch
|
||||
git apply --check "$patch"
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ $APPLY -eq 1 ]]; then
|
||||
# apply patches
|
||||
echo "APPLY Patches"
|
||||
for patch in "$patches_dir"/*.patch; do
|
||||
if git apply "$patch"; then
|
||||
echo "Applied $patch successfully."
|
||||
rm "$patch"
|
||||
else
|
||||
echo "Failed to apply $patch. Skipping."
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Return to original directory
|
||||
cd "$orig_dir" || exit 1
|
||||
Executable
+124
@@ -0,0 +1,124 @@
|
||||
#!/bin/bash
|
||||
|
||||
###############################################################################
|
||||
# Script Name: add-gnome-shortcut.sh
|
||||
#
|
||||
# Description:
|
||||
# Creates a new custom keyboard shortcut in GNOME by automatically finding
|
||||
# the next available custom-keybinding slot and registering the shortcut.
|
||||
#
|
||||
# The script:
|
||||
# 1. Reads existing GNOME custom keyboard shortcuts.
|
||||
# 2. Finds the first unused custom<N> entry.
|
||||
# 3. Adds the new shortcut to the GNOME keybinding list.
|
||||
# 4. Sets the shortcut name.
|
||||
# 5. Sets the command to execute.
|
||||
# 6. Sets the keyboard shortcut combination.
|
||||
#
|
||||
# Requirements:
|
||||
# - GNOME Desktop Environment
|
||||
# - gsettings
|
||||
#
|
||||
# Usage:
|
||||
# ./add-gnome-shortcut.sh <name> <command> <key_combo>
|
||||
#
|
||||
# Arguments:
|
||||
# <name>
|
||||
# Display name shown in GNOME Settings.
|
||||
#
|
||||
# <command>
|
||||
# Command that will be executed when the shortcut is pressed.
|
||||
#
|
||||
# <key_combo>
|
||||
# GNOME keybinding string.
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# Open Terminal:
|
||||
# ./add-gnome-shortcut.sh \
|
||||
# "Terminal" \
|
||||
# "gnome-terminal" \
|
||||
# "<Ctrl><Alt>T"
|
||||
#
|
||||
# Launch Firefox:
|
||||
# ./add-gnome-shortcut.sh \
|
||||
# "Firefox" \
|
||||
# "firefox" \
|
||||
# "<Super>F"
|
||||
#
|
||||
# Notes:
|
||||
# - Existing shortcuts are preserved.
|
||||
# - The next available custom-keybinding ID is selected automatically.
|
||||
# - Shortcut configuration can be viewed in:
|
||||
# Settings → Keyboard → Keyboard Shortcuts → Custom Shortcuts
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
if [ $# -lt 3 ]; then
|
||||
echo "Usage: $0 <new_name> <new_cmd> <key_combo>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
counter=0
|
||||
list="["
|
||||
num=0
|
||||
|
||||
# Find next available custom keybinding slot
|
||||
while read -r line; do
|
||||
list="$list$(echo "$line" | sed 's/\]//g' | sed 's/\[//g'), "
|
||||
|
||||
ret="$(printf "%s\n" "$line" | grep "custom$counter")"
|
||||
|
||||
if [ -z "$ret" ]; then
|
||||
num=$counter
|
||||
break
|
||||
else
|
||||
((counter++))
|
||||
num=$counter
|
||||
fi
|
||||
done < <(
|
||||
gsettings get \
|
||||
org.gnome.settings-daemon.plugins.media-keys \
|
||||
custom-keybindings \
|
||||
| sed 's/, /\n/g'
|
||||
)
|
||||
|
||||
key="org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:"
|
||||
custom="/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom$num/"
|
||||
|
||||
if [[ $list = *"@as"* ]]; then
|
||||
list="['$custom']"
|
||||
else
|
||||
list="$list'$custom']"
|
||||
fi
|
||||
|
||||
echo "$list"
|
||||
|
||||
new_name=$1
|
||||
new_cmd=$2
|
||||
key_combo=$3
|
||||
|
||||
echo "$key$custom $new_name $new_cmd $key_combo"
|
||||
|
||||
# Optional helper for determining key names:
|
||||
#
|
||||
# key_str="$(xev)"
|
||||
# echo "$key_str" \
|
||||
# | sed 's/KeyRelease/\nKeyRelease/g' \
|
||||
# | sed 's/KeyPress/\nKeyPress/g' \
|
||||
# | grep KeyPress \
|
||||
# | sed 's/keysym/\nkeysym/g' \
|
||||
# | sed 's/same_screen/\nsame_screen/g' \
|
||||
# | grep keysym \
|
||||
# | grep -o -P '(?<=, ).*(?=\),)' \
|
||||
# | sed 's/_L//g' \
|
||||
# | sed 's/_R//g'
|
||||
|
||||
gsettings set \
|
||||
org.gnome.settings-daemon.plugins.media-keys \
|
||||
custom-keybindings \
|
||||
"$list"
|
||||
|
||||
gsettings set "$key$custom" name "$new_name"
|
||||
gsettings set "$key$custom" command "$new_cmd"
|
||||
gsettings set "$key$custom" binding "$key_combo"
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
if [ "$2" == "-c" ];then
|
||||
blue="\e[34m"
|
||||
red="\e[31m"
|
||||
norm="\e[0m"
|
||||
else
|
||||
blue=""
|
||||
red=""
|
||||
norm=""
|
||||
fi
|
||||
declare -a hosts=("serverhost" "workhost" "quiethost" "localhost")
|
||||
if [ "$#" -eq 0 ];then
|
||||
printf "Arguments: hosts, netw\n"
|
||||
fi
|
||||
if [ "$1" == "hosts" ];then
|
||||
for host in "${hosts[@]}"; do
|
||||
ping -c 2 -W 1 $host > /dev/null
|
||||
if [ $? -eq 0 ]; then
|
||||
printf "%s\t\t[$blue UP$norm ]\n" "$host" | sed "s/localhost/$(hostname)/g"
|
||||
else
|
||||
printf "%s\t\t[$red DOWN$norm ]\n" "$host"
|
||||
fi
|
||||
done
|
||||
elif [ "$1" == "netw" ];then
|
||||
if [ "$(ifconfig | grep -A2 eth0 | grep inet)" != "" ]; then
|
||||
if="eth0"
|
||||
elif [ "$(ifconfig | grep -A2 wlan0 | grep inet)" != "" ]; then
|
||||
if="wlan0"
|
||||
fi
|
||||
#netw="$(ifconfig $if | grep -m1 -o -P '(?<=inet ).*(?= netmask)' | grep -o '[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.')"
|
||||
netw="$( ifconfig $if | grep -o '[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.' | grep -m1 -v 255)"
|
||||
for i in {1..254}; do
|
||||
ping -c 1 -W 1 $netw$i > /dev/null
|
||||
if [ $? -eq 0 ]; then
|
||||
# hostname="$(ssh 10.0.0.$i hostname)"
|
||||
# if [ ! $? -eq 0 ]; then
|
||||
hostname="$(nbtscan 10.0.0.$i | grep -A1 "\-\-\-\-\-" | sed 's/ */ /g' | cut -d' ' -f2 | grep '[A-Za-z]')"
|
||||
# fi
|
||||
printf "10.0.0.%s\t[$blue UP$norm ]\t%s\n" "$i" "$hostname"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
Executable
+81
@@ -0,0 +1,81 @@
|
||||
#!/bin/bash
|
||||
|
||||
###############################################################################
|
||||
# Script Name: crop_images.sh
|
||||
#
|
||||
# Description:
|
||||
# Safely processes JPEG images in the current directory. If an image has a
|
||||
# width of 3546 pixels, the script:
|
||||
# 1. Creates a cropped copy (2448x3264 from the top-left corner).
|
||||
# 2. Saves the cropped image as *_c.jpg.
|
||||
# 3. Renames the original image as *_u.jpg.
|
||||
#
|
||||
# Features:
|
||||
# - Processes only .jpg files.
|
||||
# - Handles filenames containing spaces.
|
||||
# - Verifies that ImageMagick is installed.
|
||||
# - Skips files that have already been processed.
|
||||
# - Uses ImageMagick's format output instead of parsing command output.
|
||||
#
|
||||
# Requirements:
|
||||
# - Bash
|
||||
# - ImageMagick (identify, convert)
|
||||
#
|
||||
# Usage:
|
||||
# chmod +x crop_images.sh
|
||||
# ./crop_images.sh
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Verify required commands exist
|
||||
for cmd in identify convert; do
|
||||
command -v "$cmd" >/dev/null 2>&1 || {
|
||||
echo "Error: '$cmd' command not found. Install ImageMagick."
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
# Process JPEG files only
|
||||
for img in *.jpg *.JPG; do
|
||||
|
||||
# Skip if no matching files exist
|
||||
[[ -f "$img" ]] || continue
|
||||
|
||||
# Skip files already processed
|
||||
[[ "$img" =~ _c\.jpg$|_u\.jpg$ ]] && continue
|
||||
|
||||
# Get image dimensions (e.g. 3546x4728)
|
||||
size=$(identify -format "%wx%h" "$img" 2>/dev/null) || {
|
||||
echo "Skipping invalid image: $img"
|
||||
continue
|
||||
}
|
||||
|
||||
# Process only images whose width is exactly 3546 pixels
|
||||
if [[ "$size" == 3546x* ]]; then
|
||||
|
||||
echo "Processing: $img ($size)"
|
||||
|
||||
cropped="${img%.jpg}_c.jpg"
|
||||
original="${img%.jpg}_u.jpg"
|
||||
|
||||
# Handle uppercase extension
|
||||
if [[ "$img" == *.JPG ]]; then
|
||||
cropped="${img%.JPG}_c.jpg"
|
||||
original="${img%.JPG}_u.jpg"
|
||||
fi
|
||||
|
||||
# Create cropped image
|
||||
convert "$img" -crop 2448x3264+0+0 "$cropped"
|
||||
|
||||
# Rename original image
|
||||
mv "$img" "$original"
|
||||
|
||||
echo " Created: $cropped"
|
||||
echo " Renamed: $original"
|
||||
fi
|
||||
|
||||
done
|
||||
|
||||
echo "Done."
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
#!/bin/bash
|
||||
|
||||
###############################################################################
|
||||
# Script Name: get_element_spectra.sh
|
||||
#
|
||||
# Description:
|
||||
# Downloads visible-light emission spectrum images for chemical elements
|
||||
# from Wikimedia Commons and converts them into panorama wallpapers.
|
||||
#
|
||||
# For each entry in get_images.txt, the script:
|
||||
# 1. Reads element information from the input file.
|
||||
# 2. Builds a standardized output filename.
|
||||
# 3. Locates the corresponding spectrum image on Wikimedia Commons.
|
||||
# 4. Downloads the original PNG file.
|
||||
# 5. Renames the image using element metadata.
|
||||
# 6. Resizes the image to 3840 pixels wide.
|
||||
# 7. Crops the image to 3840x540 pixels.
|
||||
# 8. Moves the finished panorama wallpaper to the destination folder.
|
||||
#
|
||||
# Input Format (get_images.txt):
|
||||
# element_name atomic_number symbol
|
||||
#
|
||||
# Example:
|
||||
# hydrogen 001 H
|
||||
# helium 002 He
|
||||
# lithium 003 Li
|
||||
#
|
||||
# Output Example:
|
||||
# Elements_Panorama_001_H_Hydrogen.png
|
||||
#
|
||||
# Requirements:
|
||||
# - Bash
|
||||
# - wget
|
||||
# - ImageMagick (convert)
|
||||
# - Internet connection
|
||||
#
|
||||
# Usage:
|
||||
# chmod +x get_element_spectra.sh
|
||||
# ./get_element_spectra.sh
|
||||
#
|
||||
# Notes:
|
||||
# - The script expects get_images.txt in the current directory.
|
||||
# - Images are downloaded from Wikimedia Commons.
|
||||
# - Missing images are reported and skipped.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
while IFS= read -r line; do
|
||||
|
||||
# Build output filename:
|
||||
# <atomic_number>_<symbol>_<Element>.png
|
||||
element_name="$(
|
||||
printf "%s_%s_%s%s\n" \
|
||||
"$(echo "$line" | cut -f2 -d' ')" \
|
||||
"$(echo "$line" | cut -f3 -d' ')" \
|
||||
"$(echo "$line" | cut -f1 -d' ' | head -c 1 | tr 'a-z' 'A-Z')" \
|
||||
"$(echo "$line" | cut -f1 -d' ' | tail -c +2)"
|
||||
)"
|
||||
|
||||
# Build capitalized element name used by Wikimedia
|
||||
element="$(
|
||||
printf "%s%s\n" \
|
||||
"$(echo "$line" | cut -f1 -d' ' | head -c 1 | tr 'a-z' 'A-Z')" \
|
||||
"$(echo "$line" | cut -f1 -d' ' | tail -c +2)"
|
||||
)"
|
||||
|
||||
# Retrieve original image URL from Wikimedia Commons
|
||||
LINK="$(
|
||||
wget -qO- "https://commons.wikimedia.org/wiki/File:${element}_spectrum_visible.png" \
|
||||
| grep "Original file" \
|
||||
| grep -o 'href="[^"]*' \
|
||||
| tail -c +7
|
||||
)"
|
||||
|
||||
if [ -z "$LINK" ]; then
|
||||
echo "No spectrum image found for ${element}"
|
||||
continue
|
||||
else
|
||||
echo "Downloading ${element}: ${LINK}"
|
||||
fi
|
||||
|
||||
# Download original image
|
||||
wget "$LINK"
|
||||
|
||||
# Rename downloaded image
|
||||
mv "${element}_spectrum_visible.png" "${element_name}.png"
|
||||
|
||||
# Resize image to 3840 pixels wide
|
||||
convert "${element_name}.png" \
|
||||
-resize 3840x \
|
||||
"${element_name}.png"
|
||||
|
||||
# Crop to panorama format
|
||||
convert "${element_name}.png" \
|
||||
-crop 3840x540+0+0 \
|
||||
"${element_name}.png"
|
||||
|
||||
# Move final image to wallpaper directory
|
||||
mv "${element_name}.png" \
|
||||
"/home/lucas/Documents/Dokumente/Zeug/Wallpaper/Desktop1920x1080/Elements/Elements_Panorama_${element_name}.png"
|
||||
|
||||
done < "get_images.txt"
|
||||
Executable
+105
@@ -0,0 +1,105 @@
|
||||
#!/bin/bash
|
||||
|
||||
###############################################################################
|
||||
# Script Name: find_panoramas.sh
|
||||
#
|
||||
# Description:
|
||||
# Recursively scans a directory for JPG and PNG images and identifies
|
||||
# potential panorama images using multiple detection methods:
|
||||
#
|
||||
# 1. Aspect ratio < 0.5
|
||||
# 2. Aspect ratio > 1.8
|
||||
# 3. Width > 3 × Height
|
||||
#
|
||||
# Images already tagged as "Panorama" are ignored.
|
||||
#
|
||||
# Matching images are:
|
||||
# - Logged to a timestamped logfile
|
||||
# - Optionally copied to a review directory
|
||||
#
|
||||
# Requirements:
|
||||
# - Bash
|
||||
# - ImageMagick (identify)
|
||||
# - bc
|
||||
#
|
||||
# Usage:
|
||||
# ./find_panoramas.sh
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
IMG_DIR="/home/lucas/Pictures"
|
||||
COPY_DIR="/home/lucas/Downloads"
|
||||
|
||||
PANORAMA_AR_LT="0.5"
|
||||
PANORAMA_AR_HT="1.8"
|
||||
|
||||
DATE=$(date +"%Y%m%d_%H%M%S")
|
||||
LOGFILE="${IMG_DIR}/panorama_${DATE}.log"
|
||||
|
||||
touch "$LOGFILE"
|
||||
|
||||
write_log() {
|
||||
echo "$@"
|
||||
echo "$@" >> "$LOGFILE"
|
||||
}
|
||||
|
||||
# Verify dependencies
|
||||
for cmd in identify bc; do
|
||||
command -v "$cmd" >/dev/null 2>&1 || {
|
||||
echo "Error: '$cmd' not found"
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
find "$IMG_DIR" \
|
||||
\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" \) \
|
||||
-print0 |
|
||||
while IFS= read -r -d '' img; do
|
||||
|
||||
# Get dimensions
|
||||
dimensions=$(identify -format "%wx%h" "$img" 2>/dev/null) || {
|
||||
write_log "Skipping invalid image: $img"
|
||||
continue
|
||||
}
|
||||
|
||||
width=${dimensions%x*}
|
||||
height=${dimensions#*x}
|
||||
|
||||
# Avoid divide-by-zero
|
||||
[[ "$height" -eq 0 ]] && continue
|
||||
|
||||
# Calculate aspect ratio
|
||||
aspect_ratio=$(echo "scale=4; $height / $width" | bc -l)
|
||||
|
||||
# Skip images already marked Panorama
|
||||
if identify -verbose "$img" 2>/dev/null | grep -q "Panorama"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
panorama=false
|
||||
|
||||
# Aspect-ratio checks
|
||||
if (( $(echo "$aspect_ratio < $PANORAMA_AR_LT" | bc -l) )); then
|
||||
panorama=true
|
||||
elif (( $(echo "$aspect_ratio > $PANORAMA_AR_HT" | bc -l) )); then
|
||||
panorama=true
|
||||
fi
|
||||
|
||||
# Width > 3 × Height check
|
||||
if (( width > height * 3 )); then
|
||||
panorama=true
|
||||
fi
|
||||
|
||||
if [[ "$panorama" == true ]]; then
|
||||
|
||||
write_log "PANORAMA | ${dimensions} | AR=${aspect_ratio} | ${img}"
|
||||
|
||||
# Copy for manual review
|
||||
cp -n "$img" "$COPY_DIR/" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
done
|
||||
|
||||
write_log "Scan complete."
|
||||
Executable
+141
@@ -0,0 +1,141 @@
|
||||
#!/bin/bash
|
||||
|
||||
###############################################################################
|
||||
# Script Name: collect-wallpapers.sh
|
||||
#
|
||||
# Description:
|
||||
# Scans a directory tree for image files and prepares them for wallpaper use.
|
||||
#
|
||||
# The script:
|
||||
# 1. Searches recursively for JPG, JPEG, and PNG images.
|
||||
# 2. Renames files by replacing spaces with underscores.
|
||||
# 3. Converts JPG/JPEG images to PNG format.
|
||||
# 4. Calculates image aspect ratio and width.
|
||||
# 5. Identifies images matching a target screen aspect ratio.
|
||||
# 6. Optionally copies matching images to a wallpaper collection folder.
|
||||
#
|
||||
# Matching Criteria:
|
||||
# - Aspect ratio close to 16:9 portrait (0.5625)
|
||||
# - Configurable tolerance range
|
||||
# - Minimum image width requirement
|
||||
#
|
||||
# Requirements:
|
||||
# - Bash
|
||||
# - ImageMagick (identify, convert)
|
||||
# - bc
|
||||
#
|
||||
# Usage:
|
||||
#
|
||||
# Scan only:
|
||||
# ./collect-wallpapers.sh <image_directory>
|
||||
#
|
||||
# Scan and copy matching images:
|
||||
# ./collect-wallpapers.sh <image_directory> -m
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# Analyze images:
|
||||
# ./collect-wallpapers.sh ~/Pictures
|
||||
#
|
||||
# Collect wallpaper candidates:
|
||||
# ./collect-wallpapers.sh ~/Pictures -m
|
||||
#
|
||||
# Output:
|
||||
# Matching files are copied to:
|
||||
#
|
||||
# /data/Ubuntu_Data/Videos/.stuff/
|
||||
#
|
||||
# Notes:
|
||||
# - JPG/JPEG files are converted to PNG.
|
||||
# - Spaces in filenames are replaced with underscores.
|
||||
# - Original PNG files remain unchanged.
|
||||
# - Original JPG/JPEG files are removed after conversion.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "Usage: $0 <image_directory> [-m]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IMG_PATH="$1"
|
||||
|
||||
MOVE=false
|
||||
if [ "$2" == "-m" ]; then
|
||||
MOVE=true
|
||||
fi
|
||||
|
||||
echo "Path: $IMG_PATH"
|
||||
|
||||
find "$IMG_PATH" \( -name "*.png" -o -name "*.jp*g" \)
|
||||
|
||||
files="$(find "$IMG_PATH" \( -name "*.png" -o -name "*.jp*g" \) | wc -l)"
|
||||
|
||||
printf "%s Files, Move: %s\n" "$files" "$MOVE"
|
||||
|
||||
read -n 1 -s -r -p "Press any key to continue"
|
||||
printf "\n"
|
||||
|
||||
minImgX="1400"
|
||||
screenAR="0.5625"
|
||||
AR_tolerance="0.2"
|
||||
|
||||
counter=0
|
||||
|
||||
find "$IMG_PATH" \( -name "*.png" -o -name "*.jp*g" \) | while read -r img; do
|
||||
|
||||
((counter++))
|
||||
|
||||
object_path="$(dirname "$img")/"
|
||||
img="$(basename "$img")"
|
||||
|
||||
# Replace spaces with underscores
|
||||
if [[ "$img" == *" "* ]]; then
|
||||
new_img="$(echo "$img" | sed 's/ \+/_/g')"
|
||||
|
||||
mv "${object_path}${img}" "${object_path}${new_img}"
|
||||
|
||||
img="$new_img"
|
||||
fi
|
||||
|
||||
# Convert JPG/JPEG to PNG
|
||||
if echo "$img" | grep -qiE '\.jpe?g$'; then
|
||||
|
||||
convert "${object_path}${img}" \
|
||||
"${object_path}$(echo "$img" | sed 's/\.[jJ][pP][eE]\?[gG]$/.png/')"
|
||||
|
||||
rm "${object_path}${img}"
|
||||
|
||||
img="$(echo "$img" | sed 's/\.[jJ][pP][eE]\?[gG]$/.png/')"
|
||||
fi
|
||||
|
||||
# Get image dimensions
|
||||
dimensions="$(identify -format "%wx%h" "${object_path}${img}" 2>/dev/null)"
|
||||
|
||||
width="${dimensions%x*}"
|
||||
height="${dimensions#*x}"
|
||||
|
||||
if [[ -z "$width" || -z "$height" || "$height" -eq 0 ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Calculate aspect ratio (height / width)
|
||||
imageAR="$(echo "scale=6; $height / $width" | bc -l)"
|
||||
|
||||
# Match wallpaper criteria
|
||||
if [[ "$MOVE" == true \
|
||||
&& $(echo "$imageAR < $screenAR*(1+$AR_tolerance)" | bc -l) -eq 1 \
|
||||
&& $(echo "$imageAR > $screenAR*(1-$AR_tolerance)" | bc -l) -eq 1 \
|
||||
&& $(echo "$width > $minImgX" | bc -l) -eq 1 ]]; then
|
||||
|
||||
printf "%d/%s: %s\t%s\n" \
|
||||
"$counter" \
|
||||
"$files" \
|
||||
"$img" \
|
||||
"$imageAR"
|
||||
|
||||
cp "${object_path}${img}" \
|
||||
"/data/Ubuntu_Data/Videos/.stuff/"
|
||||
fi
|
||||
|
||||
done
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
hydrogen 1 H Hydrogen spectrum visible.png
|
||||
helium 2 He Helium spectrum visible.png
|
||||
lithium 3 Li Lithium spectrum visible.png
|
||||
beryllium 4 Be Beryllium spectrum visible.png
|
||||
boron 5 B Boron spectrum visible.png
|
||||
carbon 6 C Carbon spectrum visible.png
|
||||
nitrogen 7 N Nitrogen spectrum visible.png
|
||||
oxygen 8 O Oxygen spectrum visible.png
|
||||
fluorine 9 F Fluorine spectrum visible.png
|
||||
neon 10 Ne Neon spectrum visible.png
|
||||
sodium 11 Na Sodium spectrum visible.png
|
||||
magnesium 12 Mg Magnesium spectrum visible.png
|
||||
aluminium 13 Al Aluminium spectrum visible.png
|
||||
silicon 14 Si Silicon spectrum visible.png
|
||||
phosphorus 15 P Phosphorus spectrum visible.png
|
||||
sulfur 16 S Sulfur spectrum visible.png
|
||||
chlorine 17 Cl Chlorine spectrum visible.png
|
||||
argon 18 Ar Argon spectrum visible.png
|
||||
potassium 19 K Potassium spectrum visible.png
|
||||
calcium 20 Ca Calcium spectrum visible.png
|
||||
scandium 21 Sc Scandium spectrum visible.png
|
||||
titanium 22 Ti Titanium spectrum visible.png
|
||||
vanadium 23 V Vanadium spectrum visible.png
|
||||
chromium 24 Cr Chromium spectrum visible.png
|
||||
manganese 25 Mn Manganese spectrum visible.png
|
||||
iron 26 Fe Iron spectrum visible.png
|
||||
cobalt 27 Co Cobalt spectrum visible.png
|
||||
nickel 28 Ni Nickel spectrum visible.png
|
||||
copper 29 Cu Copper spectrum visible.png
|
||||
zinc 30 Zn Zinc spectrum visible.png
|
||||
gallium 31 Ga Gallium spectrum visible.png
|
||||
germanium 32 Ge Germanium spectrum visible.png
|
||||
arsenic 33 As Arsenic spectrum visible.png
|
||||
selenium 34 Se Selenium spectrum visible.png
|
||||
bromine 35 Br Bromine spectrum visible.png
|
||||
krypton 36 Kr Krypton spectrum visible.png
|
||||
rubidium 37 Rb Rubidium spectrum visible.png
|
||||
strontium 38 Sr Strontium spectrum visible.png
|
||||
yttrium 39 Y Yttrium spectrum visible.png
|
||||
zirconium 40 Zr Zirconium spectrum visible.png
|
||||
niobium 41 Nb Niobium spectrum visible.png
|
||||
molybdenum 42 Mo Molybdenum spectrum visible.png
|
||||
technetium 43 Tc Technetium spectrum visible.png
|
||||
ruthenium 44 Ru Ruthenium spectrum visible.png
|
||||
rhodium 45 Rh Rhodium spectrum visible.png
|
||||
palladium 46 Pd Palladium spectrum visible.png
|
||||
silver 47 Ag Silver spectrum visible.png
|
||||
cadmium 48 Cd Cadmium spectrum visible.png
|
||||
indium 49 In Indium spectrum visible.png
|
||||
tin 50 Sn Tin spectrum visible.png
|
||||
antimony 51 Sb Antimony spectrum visible.png
|
||||
tellurium 52 Te Tellurium spectrum visible.png
|
||||
iodine 53 I Iodine spectrum visible.png
|
||||
xenon 54 Xe Xenon spectrum visible.png
|
||||
caesium 55 Cs Caesium spectrum visible.png
|
||||
barium 56 Ba Barium spectrum visible.png
|
||||
lanthanum 57 La Lanthanum spectrum visible.png
|
||||
cerium 58 Ce Cerium spectrum visible.png
|
||||
praseodymium 59 Pr Praseodymium spectrum visible.png
|
||||
neodymium 60 Nd Neodymium spectrum visible.png
|
||||
promethium 61 Pm Promethium spectrum visible.png
|
||||
samarium 62 Sm Samarium spectrum visible.png
|
||||
europium 63 Eu Europium spectrum visible.png
|
||||
gadolinium 64 Gd Gadolinium spectrum visible.png
|
||||
terbium 65 Tb Terbium spectrum visible.png
|
||||
dysprosium 66 Dy Dysprosium spectrum visible.png
|
||||
holmium 67 Ho Holmium spectrum visible.png
|
||||
erbium 68 Er Erbium spectrum visible.png
|
||||
thulium 69 Tm Thulium spectrum visible.png
|
||||
ytterbium 70 Yb Ytterbium spectrum visible.png
|
||||
lutetium 71 Lu Lutetium spectrum visible.png
|
||||
hafnium 72 Hf Hafnium spectrum visible.png
|
||||
tantalum 73 Ta Tantalum spectrum visible.png
|
||||
tungsten 74 W Tungsten spectrum visible.png
|
||||
rhenium 75 Re Rhenium spectrum visible.png
|
||||
osmium 76 Os Osmium spectrum visible.png
|
||||
iridium 77 Ir Iridium spectrum visible.png
|
||||
platinum 78 Pt Platinum spectrum visible.png
|
||||
gold 79 Au Gold spectrum visible.png
|
||||
mercury 80 Hg Mercury spectrum visible.png
|
||||
thallium 81 Tl Thallium spectrum visible.png
|
||||
lead 82 Pb Lead spectrum visible.png
|
||||
bismuth 83 Bi Bismuth spectrum visible.png
|
||||
polonium 84 Po Polonium spectrum visible.png
|
||||
radon 86 Rn Radon spectrum visible.png
|
||||
radium 88 Ra Radium spectrum visible.png
|
||||
actinium 89 Ac Actinium spectrum visible.png
|
||||
thorium 90 Th Thorium spectrum visible.png
|
||||
protactinium 91 Pa Protactinium spectrum visible.png
|
||||
uranium 92 U Uranium spectrum visible.png
|
||||
neptunium 93 Np Neptunium spectrum visible.png
|
||||
plutonium 94 Pu Plutonium spectrum visible.png
|
||||
americium 95 Am Americium spectrum visible.png
|
||||
curium 96 Cm Curium spectrum visible.png
|
||||
berkelium 97 Bk Berkelium spectrum visible.png
|
||||
californium 98 Cf Californium spectrum visible.png
|
||||
einsteinium 99 Es
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
|
||||
###############################################################################
|
||||
# Script Name: jpg-to-png.sh
|
||||
#
|
||||
# Description:
|
||||
# Recursively scans the current directory for JPG and JPEG images,
|
||||
# converts each image to PNG format using ImageMagick, and removes
|
||||
# the original JPG/JPEG file after a successful conversion.
|
||||
#
|
||||
# The script preserves the original directory structure and creates
|
||||
# PNG files alongside the source images.
|
||||
#
|
||||
# Requirements:
|
||||
# - Bash
|
||||
# - ImageMagick (convert)
|
||||
#
|
||||
# Usage:
|
||||
# Convert all JPG/JPEG files under the current directory:
|
||||
#
|
||||
# ./jpg-to-png.sh
|
||||
#
|
||||
# Example:
|
||||
# Before:
|
||||
# photo.jpg
|
||||
# images/picture.jpeg
|
||||
#
|
||||
# After:
|
||||
# photo.png
|
||||
# images/picture.png
|
||||
#
|
||||
# Notes:
|
||||
# - Original JPG/JPEG files are deleted after conversion.
|
||||
# - Existing PNG files with the same name may be overwritten.
|
||||
# - The script processes files recursively.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
IMG_FOLDER="."
|
||||
|
||||
# Find all JPG/JPEG files recursively
|
||||
IMG_ARR=($(find "${IMG_FOLDER}" -iname '*.jpg' -o -iname '*.jpeg'))
|
||||
|
||||
for IMG in "${IMG_ARR[@]}"; do
|
||||
|
||||
echo "Converting: ${IMG}"
|
||||
|
||||
# Convert JPG/JPEG to PNG
|
||||
convert "$IMG" "$(echo "$IMG" | sed 's/\.[jJ][pP][eE]\?[gG]$/.png/')"
|
||||
|
||||
# Remove original image
|
||||
rm "$IMG"
|
||||
|
||||
done
|
||||
Executable
+115
@@ -0,0 +1,115 @@
|
||||
#!/bin/bash
|
||||
|
||||
###############################################################################
|
||||
# Script Name: rename-images-by-date.sh
|
||||
#
|
||||
# Description:
|
||||
# Renames image files using their original capture timestamp.
|
||||
#
|
||||
# The script:
|
||||
# 1. Scans the target directory for image files.
|
||||
# 2. Reads the EXIF DateTimeOriginal tag using exiftool.
|
||||
# 3. Falls back to the file modification timestamp if EXIF data
|
||||
# is unavailable.
|
||||
# 4. Renames files to the format:
|
||||
#
|
||||
# IMG_YYYYMMDD_HHMMSS.ext
|
||||
#
|
||||
# 5. Automatically resolves filename collisions by appending a
|
||||
# numeric suffix.
|
||||
#
|
||||
# Requirements:
|
||||
# - Bash
|
||||
# - exiftool
|
||||
#
|
||||
# Usage:
|
||||
#
|
||||
# Process current directory:
|
||||
# ./rename-images-by-date.sh
|
||||
#
|
||||
# Process a specific directory:
|
||||
# ./rename-images-by-date.sh /path/to/images
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# ./rename-images-by-date.sh ~/Pictures
|
||||
#
|
||||
# Result:
|
||||
#
|
||||
# Before:
|
||||
# DSC00123.JPG
|
||||
# Vacation.png
|
||||
#
|
||||
# After:
|
||||
# IMG_20230821_143210.JPG
|
||||
# IMG_20240106_094533.png
|
||||
#
|
||||
# Notes:
|
||||
# - Uses EXIF DateTimeOriginal when available.
|
||||
# - Falls back to the file modification time when EXIF metadata is
|
||||
# missing.
|
||||
# - Preserves the original file extension.
|
||||
# - Prevents overwriting existing files.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
# Change to target directory
|
||||
TARGET_DIR="./"
|
||||
|
||||
if [[ -n "$1" ]]; then
|
||||
TARGET_DIR="$1"
|
||||
fi
|
||||
|
||||
cd "$TARGET_DIR" || exit 1
|
||||
|
||||
for file in *.{jpg,jpeg,JPG,JPEG,png,PNG}; do
|
||||
|
||||
# Skip non-existing matches
|
||||
[ -f "$file" ] || continue
|
||||
|
||||
# Extract original capture timestamp from EXIF metadata
|
||||
datetime=$(exiftool -DateTimeOriginal -s3 "$file")
|
||||
|
||||
if [[ -n "$datetime" ]]; then
|
||||
|
||||
# Convert:
|
||||
# 2023:08:21 14:32:10
|
||||
# into:
|
||||
# 20230821_143210
|
||||
formatted=$(
|
||||
echo "$datetime" |
|
||||
sed 's/://g' |
|
||||
sed 's/ /_/g' |
|
||||
cut -c1-15
|
||||
)
|
||||
|
||||
echo "$formatted"
|
||||
|
||||
else
|
||||
|
||||
# Fallback: use file modification timestamp
|
||||
formatted=$(date -r "$file" +"%Y%m%d_%H%M%S")
|
||||
|
||||
fi
|
||||
|
||||
extension="${file##*.}"
|
||||
|
||||
newname="IMG_${formatted}.${extension}"
|
||||
|
||||
# Prevent overwriting existing files
|
||||
if [[ -e "$newname" ]]; then
|
||||
|
||||
i=1
|
||||
|
||||
while [[ -e "IMG_${formatted}_$i.${extension}" ]]; do
|
||||
((i++))
|
||||
done
|
||||
|
||||
newname="IMG_${formatted}_$i.${extension}"
|
||||
fi
|
||||
|
||||
echo "Renaming: '$file' -> '$newname'"
|
||||
|
||||
mv "$file" "$newname"
|
||||
|
||||
done
|
||||
Executable
+98
@@ -0,0 +1,98 @@
|
||||
#!/bin/bash
|
||||
|
||||
###############################################################################
|
||||
# Script Name: resize-images.sh
|
||||
#
|
||||
# Description:
|
||||
# Resizes JPG/JPEG images and converts them to PNG format.
|
||||
#
|
||||
# When no resize argument is supplied:
|
||||
# - Portrait images are resized to fit within 1080px height (x1080)
|
||||
# - Landscape images are resized to fit within 1920px width (1920)
|
||||
#
|
||||
# When a resize argument is supplied:
|
||||
# - The supplied resize value is used for all images.
|
||||
#
|
||||
# Additional features:
|
||||
# - Creates output directory NEW/ automatically.
|
||||
# - Optionally normalizes filenames by replacing spaces with underscores.
|
||||
# - Adds an optional date prefix to generated filenames.
|
||||
# - Converts JPG/JPEG images to PNG.
|
||||
#
|
||||
# Requirements:
|
||||
# - Bash
|
||||
# - ImageMagick (identify, convert)
|
||||
#
|
||||
# Usage:
|
||||
#
|
||||
# Automatic portrait/landscape sizing:
|
||||
# ./resize-images.sh
|
||||
#
|
||||
# Resize all images to width 2560:
|
||||
# ./resize-images.sh 2560
|
||||
#
|
||||
# Resize all images to height 1440:
|
||||
# ./resize-images.sh x1440
|
||||
#
|
||||
# Output:
|
||||
# NEW/<date>filename.png
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
DATE=$(date +"%d%m%y_")
|
||||
DEFAULT_RESIZE="auto"
|
||||
|
||||
# Use supplied resize value or automatic mode
|
||||
if [[ -n "${1:-}" ]]; then
|
||||
DEFAULT_RESIZE="$1"
|
||||
fi
|
||||
|
||||
# Create output directory
|
||||
mkdir -p NEW
|
||||
|
||||
for file in *.jpg *.jpeg *.JPG *.JPEG; do
|
||||
|
||||
[[ -f "$file" ]] || continue
|
||||
|
||||
# Replace spaces in filenames
|
||||
file_new="$(echo "$file" | tr ' ' '_')"
|
||||
|
||||
if [[ "$file_new" != "$file" ]]; then
|
||||
echo "Rename: $file -> $file_new"
|
||||
mv "$file" "$file_new"
|
||||
fi
|
||||
|
||||
resize_var="$DEFAULT_RESIZE"
|
||||
|
||||
# Automatic resize mode
|
||||
if [[ "$resize_var" == "auto" ]]; then
|
||||
|
||||
dimensions=$(identify -quiet -format "%wx%h" "$file_new" 2>/dev/null)
|
||||
|
||||
width=${dimensions%x*}
|
||||
height=${dimensions#*x}
|
||||
|
||||
if [[ -n "$width" && -n "$height" ]]; then
|
||||
if (( height > width )); then
|
||||
resize_var="x1080"
|
||||
else
|
||||
resize_var="1920"
|
||||
fi
|
||||
else
|
||||
echo "Skipping invalid image: $file_new"
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
output="NEW/${DATE}$(basename "${file_new%.*}").png"
|
||||
|
||||
echo "Transform: $file_new -> $output ($resize_var)"
|
||||
|
||||
convert -quiet \
|
||||
"$file_new" \
|
||||
-resize "$resize_var" \
|
||||
"$output"
|
||||
|
||||
done
|
||||
|
||||
echo "Finished."
|
||||
Executable
+99
@@ -0,0 +1,99 @@
|
||||
#!/bin/bash
|
||||
|
||||
###############################################################################
|
||||
# Script Name: screenshot_grab.sh
|
||||
#
|
||||
# Description:
|
||||
# Interactive screenshot capture tool for GNOME desktops.
|
||||
#
|
||||
# The script automatically determines a destination directory from an
|
||||
# open Texmaker project when possible. If no Texmaker project is detected,
|
||||
# the user is prompted to choose a destination folder manually.
|
||||
#
|
||||
# Features:
|
||||
# - Automatic Texmaker project path detection
|
||||
# - Manual directory selection fallback
|
||||
# - User-defined screenshot filenames
|
||||
# - Duplicate filename detection
|
||||
# - Automatic filename copy to clipboard
|
||||
# - Area screenshot capture using gnome-screenshot
|
||||
# - Screenshot preview/confirmation loop
|
||||
#
|
||||
# Requirements:
|
||||
# - Bash
|
||||
# - zenity
|
||||
# - gnome-screenshot
|
||||
# - xclip
|
||||
# - Texmaker (optional)
|
||||
#
|
||||
# Usage:
|
||||
# ./screenshot_grab.sh
|
||||
#
|
||||
# Workflow:
|
||||
# 1. Detect current Texmaker project folder or ask for a directory.
|
||||
# 2. Enter a screenshot filename.
|
||||
# 3. Filename is copied to the clipboard automatically.
|
||||
# 4. Select a screen area using gnome-screenshot.
|
||||
# 5. Confirm the result.
|
||||
# 6. Retake the screenshot until satisfied.
|
||||
#
|
||||
# Output:
|
||||
# <selected_directory>/<filename>.png
|
||||
#
|
||||
# Notes:
|
||||
# - The line beginning with ### contains the default fallback directory.
|
||||
# - Existing filenames are checked before capturing.
|
||||
# - Cancelling any dialog exits the script.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
###/data/DatenLucas/Dokumente/Studium/Master/3.Semester/Mechatronische Systeme/Lernunterlagen/
|
||||
DEFAULT_DIR="$(cat ~/scripts/screenshot_grab.sh | grep "###" | grep -v grep | sed 's/###//g')"
|
||||
cont=1
|
||||
PATH="$(ps -elf | grep -v grep | grep -o -P '(?<=texmaker ).*(?=/.*\.tex)')/"
|
||||
if [ "$PATH" == "/" ]; then
|
||||
# /usr/bin/zenity --error --text="No Filepath detected"
|
||||
# exit 0
|
||||
while [ $cont -eq 1 ]; do
|
||||
PATH="$(/usr/bin/zenity --file-selection --title="No TEX open: Where the FUCK shall I go???" --directory --width=600 --height=400 --filename="$DEFAULT_DIR")"
|
||||
ret=$?
|
||||
if [[ "$PATH" != "/" ]] && [[ -d "$PATH" ]] && [[ $ret -eq 0 ]]; then
|
||||
cont=0
|
||||
elif [[ $ret -eq 1 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
PATH="$PATH/"
|
||||
fi
|
||||
text="Filename exists:"
|
||||
NAME="$(/usr/bin/zenity --entry --ok-label="Fuck" --title="Filename" --text="Enter Name:")"
|
||||
while [ $cont -eq 1 ]; do
|
||||
if [ -a "$PATH$NAME.png" ]; then
|
||||
NEWNAME="$(/usr/bin/zenity --entry --entry-text="$NAME" --cancel-label="Fuck It!" --ok-label="New Fuck" --title="Shit Shit Shit" --text="$text")"
|
||||
ret=$?
|
||||
if [[ "$NEWNAME" == "" ]] && [[ $ret -eq 1 ]]; then
|
||||
cont=0
|
||||
elif [[ "$NAME" != "$NEWNAME" ]] && [[ $ret -eq 0 ]]; then
|
||||
NAME=$NEWNAME
|
||||
cont=0
|
||||
elif [[ "$NAME" == "$NEWNAME" ]] && [[ $ret -eq 0 ]]; then
|
||||
text="I said new name you stupd fucking Idiot"
|
||||
else
|
||||
/usr/bin/zenity --error --title "Fuck Fuck Fuck Error" --text='Some Fucking unexpected State'
|
||||
fi
|
||||
else
|
||||
cont=0
|
||||
fi
|
||||
done
|
||||
if [ "$NAME" == "" ]; then
|
||||
exit 1;
|
||||
fi
|
||||
printf "%s" "$NAME" | /usr/bin/xclip -sel clip
|
||||
NAME="$NAME.png"
|
||||
printf "%s\n" "$PATH$NAME"
|
||||
cont=1
|
||||
while [ $cont -eq 1 ]; do
|
||||
/usr/bin/gnome-screenshot -a -f "$PATH$NAME"
|
||||
/usr/bin/zenity --title="Confirmation" --text="Picture OK?" --ok-label="Fuck Yes" --cancel-label="Fuck No" --question
|
||||
cont=$?
|
||||
done
|
||||
Executable
+105
@@ -0,0 +1,105 @@
|
||||
#!/bin/bash
|
||||
NEW_TYPE="png"
|
||||
TYPE_ARR=("jp*g" "tif" "webp" "png")
|
||||
file=""
|
||||
folder=""
|
||||
usage(){
|
||||
printf "Usage:
|
||||
-f <file>\tConvert single File
|
||||
-d <dir>\tConvert all files in Dir
|
||||
-t <type>\tSet new Type
|
||||
-h\t\tDisplay this\n"
|
||||
}
|
||||
|
||||
img_convert() {
|
||||
IMG="$1"
|
||||
IMG_NEW="$IMG"
|
||||
for type in ${TYPE_ARR[@]}; do
|
||||
IMG_NEW="$(echo ${IMG_NEW} | sed 's/\.'"${type}"'/.'"${NEW_TYPE}"'/g')"
|
||||
done
|
||||
if [ ! -f "${IMG}" ]; then
|
||||
echo "${IMG} not found"
|
||||
return
|
||||
fi
|
||||
if [ "${IMG}" == "${IMG_NEW}" ]; then
|
||||
echo "${IMG} and ${IMG_NEW} are the same"
|
||||
return
|
||||
fi
|
||||
echo "Convert ${IMG} ${IMG_NEW}"
|
||||
convert -quiet "${IMG}" "${IMG_NEW}" >> /dev/null
|
||||
rm "${IMG}"
|
||||
}
|
||||
|
||||
folder_magic(){
|
||||
IMG_FOLDER="$1"
|
||||
echo "Convert Imgs in ${IMG_FOLDER}"
|
||||
IMG_ARR=()
|
||||
IMG_ARR_NEW=()
|
||||
|
||||
IFS=$'\n'; set -f
|
||||
for type in ${TYPE_ARR[@]}; do
|
||||
if [[ "${type}" != "${NEW_TYPE}" ]]; then
|
||||
IMG_ARR+=($(find ${IMG_FOLDER} -name "*${type}"))
|
||||
IMG_ARR_NEW+=($(find ${IMG_FOLDER} -name "*${type}" | sed 's/\.'"${type}"'/.'"${NEW_TYPE}"'/g'))
|
||||
fi
|
||||
done
|
||||
for ((i=0;i<${#IMG_ARR[@]};i++)); do
|
||||
echo "Convert ${IMG_ARR[$i]} ${IMG_ARR_NEW[$i]}"
|
||||
convert -quiet "${IMG_ARR[$i]}" "${IMG_ARR_NEW[$i]}" >> /dev/null
|
||||
rm "${IMG_ARR[$i]}"
|
||||
done
|
||||
}
|
||||
|
||||
get_config() {
|
||||
if [ $# -eq 0 ] || [ ${1:0:1} != - ]; then
|
||||
echo "Too few Arguments"
|
||||
usage
|
||||
exit
|
||||
fi
|
||||
while [[ ${1:0:1} == - ]]; do
|
||||
echo "Param: $1"
|
||||
[[ $1 == -h ]] && {
|
||||
usage
|
||||
exit
|
||||
};
|
||||
[[ $1 == -f ]] && {
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "Too few Arguments"
|
||||
usage
|
||||
exit
|
||||
fi
|
||||
file="$2";
|
||||
shift 2;
|
||||
continue;
|
||||
}
|
||||
[[ $1 == -d ]] && {
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "Too few Arguments"
|
||||
usage
|
||||
exit
|
||||
fi
|
||||
folder="$2";
|
||||
shift 2;
|
||||
continue;
|
||||
}
|
||||
[[ $1 == -t ]] && {
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "Too few Arguments"
|
||||
usage
|
||||
exit
|
||||
fi
|
||||
NEW_TYPE="$2";
|
||||
shift 2;
|
||||
continue;
|
||||
}
|
||||
echo "Parameter $1 discarded"
|
||||
shift 1;
|
||||
done
|
||||
}
|
||||
get_config "$@"
|
||||
if [[ "${file}" != "" ]]; then
|
||||
img_convert "${file}"
|
||||
fi
|
||||
if [[ "${folder}" != "" ]]; then
|
||||
folder_magic "${folder}"
|
||||
fi
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
PUB_IP=$(wget -T 5 -qO- ifconfig.co)
|
||||
printf "User: %s\n" "$(whoami)"
|
||||
if [ "${PUB_IP}" != "" ]; then
|
||||
LOC=$(wget -T 3 -qO- ifconfig.co/country)
|
||||
CITY=$(wget -T 3 -qO- ifconfig.co/city)
|
||||
printf "%s\t%s\t%s\n" "${PUB_IP}" "${LOC}" "${CITY}"
|
||||
else
|
||||
printf "No Connection\n"
|
||||
fi
|
||||
if [ "$(grep deluge-user /etc/passwd)" != "" ]; then
|
||||
printf "User: deluge-user\n"
|
||||
PUB_IP=$(sudo /home/lucas/scripts/deluge_user_cmd.sh wget -T 3 -qO- ifconfig.co)
|
||||
if [ "${PUB_IP}" == "" ]; then
|
||||
PUB_IP=$(sudo /home/lucas/scripts/deluge_user_cmd.sh curl --connect-timeout 3 https://ipinfo.io/ip)
|
||||
fi
|
||||
if [ "${PUB_IP}" != "" ]; then
|
||||
LOC=$(sudo /home/lucas/scripts/deluge_user_cmd.sh wget -T 3 -qO- ifconfig.co/country)
|
||||
if [ "${LOC}" == "" ]; then
|
||||
LOC=$(sudo /home/lucas/scripts/deluge_user_cmd.sh curl --connect-timeout 3 https://ipinfo.io/country 2>>/dev/null)
|
||||
fi
|
||||
CITY=$(sudo /home/lucas/scripts/deluge_user_cmd.sh wget -T 3 -qO- ifconfig.co/city)
|
||||
if [ "${CITY}" == "" ]; then
|
||||
CITY=$(sudo /home/lucas/scripts/deluge_user_cmd.sh curl --connect-timeout 3 https://ipinfo.io/city 2>>/dev/null)
|
||||
fi
|
||||
printf "%s\t%s\t%s\n" "${PUB_IP}" "${LOC}" "${CITY}"
|
||||
else
|
||||
printf "No Connection\n"
|
||||
fi
|
||||
fi
|
||||
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()
|
||||
Executable
+208
@@ -0,0 +1,208 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
shopt -s nullglob nocaseglob
|
||||
LOGFILE="/home/lucas/logs/convertheic.log"
|
||||
|
||||
mkdir -p "$(dirname "$LOGFILE")"
|
||||
exec >>"$LOGFILE" 2>&1
|
||||
|
||||
echo "Script started at $(date)"
|
||||
|
||||
|
||||
convert_one() {
|
||||
local src="$1"
|
||||
|
||||
if [[ ! -f "$src" ]]; then
|
||||
echo "ERROR: file not found: $src"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local base="${src%.*}"
|
||||
local dst="${base}.jpg"
|
||||
local tmp="${base}.tmp.jpg"
|
||||
|
||||
echo "Converting: $src"
|
||||
|
||||
# Convert HEIC -> JPEG
|
||||
if command -v magick >/dev/null 2>&1; then
|
||||
if ! magick "$src" \
|
||||
-auto-orient \
|
||||
-quality 100 \
|
||||
-sampling-factor 4:4:4 \
|
||||
"jpg:$tmp"; then
|
||||
echo "ERROR: local conversion failed: $src"
|
||||
rm -f "$tmp"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
if ! docker run --rm \
|
||||
-u "$(id -u):$(id -g)" \
|
||||
-v "$PWD:/work" \
|
||||
-w /work \
|
||||
--entrypoint magick \
|
||||
dpokidov/imagemagick \
|
||||
"$src" \
|
||||
-auto-orient \
|
||||
-quality 100 \
|
||||
-sampling-factor 4:4:4 \
|
||||
"jpg:$tmp"; then
|
||||
echo "ERROR: docker conversion failed: $src"
|
||||
rm -f "$tmp"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
if [[ ! -s "$tmp" ]]; then
|
||||
echo "ERROR: output JPEG is empty: $tmp"
|
||||
rm -f "$tmp"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Copying metadata..."
|
||||
|
||||
if ! exiftool \
|
||||
-overwrite_original \
|
||||
-TagsFromFile "$src" \
|
||||
-all:all \
|
||||
-unsafe \
|
||||
"$tmp" >/dev/null; then
|
||||
echo "ERROR: metadata copy failed: $src"
|
||||
rm -f "$tmp"
|
||||
return 1
|
||||
fi
|
||||
|
||||
touch -r "$src" "$tmp"
|
||||
|
||||
mv -f "$tmp" "$dst"
|
||||
|
||||
if [[ ! -s "$dst" ]]; then
|
||||
echo "ERROR: final JPEG missing: $dst"
|
||||
return 1
|
||||
fi
|
||||
|
||||
rm -f "$src"
|
||||
|
||||
echo "Done: $dst"
|
||||
echo "Removed: $src"
|
||||
}
|
||||
convert_mov() {
|
||||
local src="$1"
|
||||
|
||||
if [[ ! -f "$src" ]]; then
|
||||
echo "ERROR: file not found: $src"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local base="${src%.*}"
|
||||
local dst="${base}.mp4"
|
||||
local tmp="${base}.tmp.mp4"
|
||||
|
||||
echo "Converting: $src"
|
||||
|
||||
# MOV -> MP4, keep video/audio, drop unsupported data streams
|
||||
if ! ffmpeg \
|
||||
-hide_banner \
|
||||
-loglevel error \
|
||||
-i "$src" \
|
||||
-map 0:0 \
|
||||
-map 0:1 \
|
||||
-map_metadata 0 \
|
||||
-c copy \
|
||||
-movflags use_metadata_tags+faststart \
|
||||
"$tmp"; then
|
||||
echo "ERROR: conversion failed: $src"
|
||||
rm -f "$tmp"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -s "$tmp" ]]; then
|
||||
echo "ERROR: output MP4 is empty: $tmp"
|
||||
rm -f "$tmp"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Copying metadata..."
|
||||
|
||||
if ! exiftool \
|
||||
-overwrite_original \
|
||||
-TagsFromFile "$src" \
|
||||
-all:all \
|
||||
-unsafe \
|
||||
"$tmp" >/dev/null; then
|
||||
echo "ERROR: metadata copy failed: $src"
|
||||
rm -f "$tmp"
|
||||
return 1
|
||||
fi
|
||||
|
||||
touch -r "$src" "$tmp"
|
||||
|
||||
mv -f "$tmp" "$dst"
|
||||
|
||||
if [[ ! -s "$dst" ]]; then
|
||||
echo "ERROR: final MP4 missing: $dst"
|
||||
return 1
|
||||
fi
|
||||
|
||||
rm -f "$src"
|
||||
|
||||
echo "Done: $dst"
|
||||
echo "Removed: $src"
|
||||
}
|
||||
|
||||
process_file() {
|
||||
case "${1,,}" in
|
||||
*.heic)
|
||||
convert_one "$1"
|
||||
;;
|
||||
*.mov)
|
||||
convert_mov "$1"
|
||||
;;
|
||||
*)
|
||||
echo "Skipping unsupported file: $1"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
HOSTNAME_EXPECTED="mediahost"
|
||||
SOURCE_DIR="/data/nextcloud/syncuser/alina/Dropfolder"
|
||||
TARGET_DIR="/data/nextcloud/syncuser/camera"
|
||||
|
||||
# If running on serverhost, perform initial sync
|
||||
if [[ "$(hostname)" == "$HOSTNAME_EXPECTED" ]]; then
|
||||
syncNC -d -c
|
||||
cd "$SOURCE_DIR" || exit 1
|
||||
fi
|
||||
|
||||
|
||||
if [[ $# -gt 0 ]]; then
|
||||
process_file "$1"
|
||||
else
|
||||
shopt -s nullglob
|
||||
files=( *.heic *.mov )
|
||||
|
||||
if [[ ${#files[@]} -eq 0 ]]; then
|
||||
echo "No HEIC or MOV files found."
|
||||
else
|
||||
for f in "${files[@]}"; do
|
||||
process_file "$f"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run sync again if on serverhost
|
||||
if [[ "$(hostname)" == "$HOSTNAME_EXPECTED" ]]; then
|
||||
shopt -s nullglob
|
||||
for ext in jpg mp4; do
|
||||
files=("$SOURCE_DIR"/*."$ext")
|
||||
if ((${#files[@]})); then
|
||||
mv -n "${files[@]}" "$TARGET_DIR"/
|
||||
fi
|
||||
done
|
||||
shopt -u nullglob
|
||||
|
||||
syncNC -d -c
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,392 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def check_command(command):
|
||||
if shutil.which(command) is None:
|
||||
print(f"ERROR: '{command}' is not installed.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def scan_dvd(dvd_name: Path):
|
||||
print("Scanning DVD")
|
||||
print(dvd_name)
|
||||
|
||||
result = subprocess.run(
|
||||
["lsdvd", dvd_name],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
output = result.stdout
|
||||
|
||||
pattern = re.compile(
|
||||
r"Title:\s*(\d+),\s*Length:\s*(\d+):(\d+):(\d+(?:\.\d+)?)"
|
||||
)
|
||||
|
||||
titles = []
|
||||
|
||||
for match in pattern.finditer(output):
|
||||
title = int(match.group(1))
|
||||
|
||||
hours = int(match.group(2))
|
||||
minutes = int(match.group(3))
|
||||
seconds = float(match.group(4))
|
||||
|
||||
duration = (
|
||||
hours * 3600
|
||||
+ minutes * 60
|
||||
+ seconds
|
||||
)
|
||||
|
||||
titles.append({
|
||||
"title": title,
|
||||
"duration": duration,
|
||||
})
|
||||
|
||||
if not titles:
|
||||
raise RuntimeError("lsdvd found no DVD titles")
|
||||
|
||||
|
||||
|
||||
return titles
|
||||
|
||||
def get_audio_tracks(dvd, title):
|
||||
"""Return HandBrake audio track information for a DVD title."""
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
"HandBrakeCLI",
|
||||
"-i", dvd,
|
||||
"-t", str(title),
|
||||
"--scan",
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
output = result.stdout
|
||||
|
||||
# Examples HandBrake may produce:
|
||||
#
|
||||
# + 1, Deutsch (AC3, 2.0 ch, 192 kbps)
|
||||
# + 2, English (AC3, 2.0 ch, 224 kbps)
|
||||
#
|
||||
# Also handles German/Englisch/deu/eng where present.
|
||||
|
||||
tracks = []
|
||||
|
||||
pattern = re.compile(
|
||||
r"\+\s+(\d+),\s+(.+?)\s+\((?:AC3|AAC|MP3|E-AC-3|DTS)"
|
||||
)
|
||||
|
||||
for match in pattern.finditer(output):
|
||||
track_number = int(match.group(1))
|
||||
description = match.group(2).strip()
|
||||
|
||||
tracks.append({
|
||||
"number": track_number,
|
||||
"description": description,
|
||||
})
|
||||
if not tracks:
|
||||
tracks.append({
|
||||
"number": 0,
|
||||
"description": "",
|
||||
})
|
||||
return tracks
|
||||
|
||||
def choose_audio_tracks(tracks):
|
||||
"""
|
||||
Select German first, English second.
|
||||
|
||||
Returns a list of HandBrake track numbers.
|
||||
"""
|
||||
|
||||
german = None
|
||||
english = None
|
||||
unkown = None
|
||||
for track in tracks:
|
||||
text = track["description"].lower()
|
||||
if not text:
|
||||
track["description"] = "Unknown"
|
||||
unkown = track["number"]
|
||||
# German
|
||||
if any(x in text for x in (
|
||||
"german",
|
||||
"deutsch",
|
||||
"deu",
|
||||
)):
|
||||
german = track["number"]
|
||||
|
||||
# English
|
||||
if any(x in text for x in (
|
||||
"english",
|
||||
"englisch",
|
||||
"eng",
|
||||
)):
|
||||
english = track["number"]
|
||||
|
||||
selected = []
|
||||
|
||||
# German FIRST -> becomes default audio
|
||||
if german is not None:
|
||||
selected.append(german)
|
||||
|
||||
# English SECOND
|
||||
if english is not None and english != german:
|
||||
selected.append(english)
|
||||
|
||||
if english is None and german is None:
|
||||
selected.append(unkown)
|
||||
|
||||
return selected
|
||||
|
||||
def find_episode_count(dvd_name: Path, max_dvd_length: int=70):
|
||||
"""Find the number of episodes using lsdvd.
|
||||
|
||||
Assumes:
|
||||
- The longest title is a combined track containing all episodes.
|
||||
- Individual episode titles immediately follow the longest title.
|
||||
- Episode titles have approximately similar durations.
|
||||
"""
|
||||
|
||||
titles = scan_dvd(dvd_name)
|
||||
sorted_items = sorted(titles, key=lambda x: x["duration"], reverse=True)
|
||||
combined_title = 0
|
||||
if sorted_items[0]["duration"] > sorted_items[1]["duration"] * 2:
|
||||
# Longest title = combined episode track
|
||||
combined = max(titles, key=lambda x: x["duration"])
|
||||
combined_title = combined["title"]
|
||||
|
||||
# Episode titles must immediately follow the combined title.
|
||||
title_map = {
|
||||
t["title"]: t["duration"]
|
||||
for t in titles
|
||||
}
|
||||
|
||||
first_episode = combined_title + 1
|
||||
|
||||
episode_titles = []
|
||||
|
||||
for title in range(first_episode, max(title_map) + 1):
|
||||
if title not in title_map:
|
||||
break
|
||||
|
||||
duration = title_map[title]
|
||||
|
||||
# Typical episode is ~20-35 minutes.
|
||||
# Adjust these limits if your DVDs contain longer/shorter episodes.
|
||||
if 15 * 60 <= duration <= max_dvd_length * 60:
|
||||
episode_titles.append(title)
|
||||
else:
|
||||
continue
|
||||
|
||||
if not episode_titles:
|
||||
raise RuntimeError(
|
||||
f"Could not find episodes after combined title "
|
||||
f"{first_episode}"
|
||||
)
|
||||
first_episode = episode_titles[0]
|
||||
if combined_title > 0:
|
||||
print(f"Combined title: {titles[combined_title]["title"]}")
|
||||
print(f"Combined duration: {titles[combined_title]["duration"]:.1f}s")
|
||||
print(f"Found {len(episode_titles)} episodes:")
|
||||
print(f" Titles: {episode_titles}")
|
||||
|
||||
return first_episode, len(episode_titles), episode_titles
|
||||
|
||||
def process_dvd(dvd_source: Path,dvd_title: str,num_of_episodes: int, first_episode: int, args):
|
||||
|
||||
output_dir = Path("~/Downloads").expanduser() / dvd_source.parent.name
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
titles = scan_dvd(dvd_source)
|
||||
last_episode = first_episode + num_of_episodes
|
||||
|
||||
available_titles = {t["title"] for t in titles}
|
||||
|
||||
missing = [
|
||||
title
|
||||
for title in range(first_episode, last_episode )
|
||||
if title not in available_titles
|
||||
]
|
||||
|
||||
if missing:
|
||||
print(
|
||||
"ERROR: Expected episode titles were not found:",
|
||||
", ".join(map(str, missing)),
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
print("Episodes to extract:")
|
||||
for number in range(1, num_of_episodes + 1):
|
||||
title = first_episode + number - 1
|
||||
print(f" Episode {number}: DVD title {title}")
|
||||
|
||||
print()
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
print("Output directory:")
|
||||
print(f" {output_dir}")
|
||||
print()
|
||||
|
||||
for episode in range(first_episode, first_episode + num_of_episodes ):
|
||||
title = episode
|
||||
title_from_zero = episode - first_episode +1
|
||||
if title_from_zero in args.skip_episodes:
|
||||
continue
|
||||
|
||||
output_file = output_dir / f"{dvd_title} - {title_from_zero:02d}.mp4"
|
||||
|
||||
if output_file.exists():
|
||||
size_mb = output_file.stat().st_size / (1024 * 1024)
|
||||
print(f"{output_file} ({size_mb:.1f} MB)")
|
||||
audio_tracks = choose_audio_tracks(get_audio_tracks(dvd_source, title))
|
||||
|
||||
if not audio_tracks:
|
||||
raise RuntimeError(
|
||||
f"No German or English audio track found for title {title}"
|
||||
)
|
||||
|
||||
|
||||
print("=" * 50)
|
||||
print(f"Episode {episode-first_episode +1} / {num_of_episodes}")
|
||||
print(f"DVD title: {title}")
|
||||
print(f"Output: {output_file}")
|
||||
print("=" * 50)
|
||||
|
||||
command = [
|
||||
"HandBrakeCLI",
|
||||
"-i", dvd_source,
|
||||
"-t", str(title),
|
||||
"-o", output_file,
|
||||
"-e", "x264",
|
||||
"-q", "18",
|
||||
"-a", ",".join(map(str, audio_tracks)),
|
||||
"-E", "copy:ac3",
|
||||
]
|
||||
with open("handbrake.log", "w") as logfile:
|
||||
try:
|
||||
subprocess.run(
|
||||
command,
|
||||
stdout=logfile,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
print(
|
||||
f"ERROR: HandBrakeCLI failed for episode {episode}.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(exc.returncode)
|
||||
|
||||
print()
|
||||
|
||||
|
||||
|
||||
print("=" * 50)
|
||||
print("Finished!")
|
||||
print("=" * 50)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Extract episodes from a DVD using lsdvd and HandBrakeCLI."
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"dvd",
|
||||
help="Path to DVD directory containing VIDEO_TS",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--episodes",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Number of episodes to extract (0 = auto-detect)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-episodes",
|
||||
type=int,nargs="+",
|
||||
default=[],
|
||||
help="List of episodes to skip",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
dvd_name = os.path.abspath(args.dvd)
|
||||
|
||||
check_command("lsdvd")
|
||||
check_command("HandBrakeCLI")
|
||||
|
||||
root = Path(dvd_name).resolve()
|
||||
|
||||
check_command("lsdvd")
|
||||
check_command("HandBrakeCLI")
|
||||
|
||||
# Find all VIDEO_TS directories
|
||||
video_ts_dirs = [
|
||||
p for p in root.rglob("VIDEO_TS")
|
||||
if p.is_dir()
|
||||
]
|
||||
|
||||
# Find all ISO files
|
||||
iso_files = [
|
||||
p for p in root.rglob("*")
|
||||
if p.is_file() and p.suffix.lower() == ".iso"
|
||||
]
|
||||
|
||||
# Process VIDEO_TS directories
|
||||
for video_ts in video_ts_dirs:
|
||||
dvd_source = video_ts
|
||||
|
||||
# Parent of VIDEO_TS, e.g.
|
||||
# /movies/Pippi DVD 1/VIDEO_TS
|
||||
# -> Pippi DVD 1
|
||||
dvd_title = video_ts.parent.name
|
||||
|
||||
print(f"Found DVD: {dvd_title}")
|
||||
print(f" Source: {dvd_source}")
|
||||
first_episode, episode_count, episode_titles = find_episode_count(dvd_source)
|
||||
process_dvd(
|
||||
dvd_source,
|
||||
dvd_title,
|
||||
episode_count,
|
||||
first_episode,
|
||||
args
|
||||
)
|
||||
|
||||
# Process ISO files
|
||||
for iso in iso_files:
|
||||
dvd_source = str(iso)
|
||||
|
||||
# Filename without .iso
|
||||
dvd_title = iso.stem
|
||||
|
||||
print(f"Found ISO: {dvd_title}")
|
||||
print(f" Source: {dvd_source}")
|
||||
first_episode, episode_count, episode_titles = find_episode_count(dvd_source)
|
||||
process_dvd(
|
||||
dvd_source,
|
||||
dvd_title,
|
||||
episode_count,
|
||||
first_episode,
|
||||
args
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
cd /docker
|
||||
find . -maxdepth 3 -type f \
|
||||
\( -name "*.yaml" -o -name "*.yml" -o -name "*.conf" -o -name "*.sh" \) \
|
||||
-print0 2>/dev/null |
|
||||
rsync -aR --files-from=- --from0 ./ /home/lucas/scripts/docker_compose/$(hostname)
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
sudo cscli alerts list
|
||||
sudo cscli decisions list
|
||||
watch -n5 "sudo cscli metrics"
|
||||
# Current bans
|
||||
sudo cscli decisions list
|
||||
|
||||
# Recent alerts
|
||||
sudo cscli alerts list
|
||||
|
||||
# Detailed alert information
|
||||
sudo cscli alerts inspect <ID>
|
||||
|
||||
# Metrics
|
||||
sudo cscli metrics
|
||||
|
||||
# Registered bouncers
|
||||
sudo cscli bouncers list
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
|
||||
OUT="wifi_debug_$(date +%Y%m%d_%H%M%S).txt"
|
||||
|
||||
{
|
||||
echo "===== DATE ====="
|
||||
date
|
||||
|
||||
echo
|
||||
echo "===== HOST ====="
|
||||
hostnamectl
|
||||
|
||||
echo
|
||||
echo "===== PUBLIC IP ====="
|
||||
curl -4 -s ifconfig.me || true
|
||||
echo
|
||||
|
||||
#echo
|
||||
#echo "===== INTERFACES ====="
|
||||
#ip addr
|
||||
#
|
||||
#echo
|
||||
#echo "===== ROUTES ====="
|
||||
#ip route
|
||||
#
|
||||
#echo
|
||||
#echo "===== DNS ====="
|
||||
#resolvectl status
|
||||
#
|
||||
#echo
|
||||
#echo "===== WIFI INFO ====="
|
||||
#nmcli device wifi
|
||||
#
|
||||
#echo
|
||||
#echo "===== ACTIVE CONNECTION ====="
|
||||
#nmcli connection show --active
|
||||
#
|
||||
#echo
|
||||
#echo "===== LINK STATUS ====="
|
||||
#iw dev
|
||||
#
|
||||
#echo
|
||||
#echo "===== RFKILL ====="
|
||||
#rfkill list
|
||||
|
||||
echo
|
||||
echo "===== GATEWAY PING ====="
|
||||
ping -c 4 $(ip route | awk '/default/ {print $3; exit}')
|
||||
|
||||
echo
|
||||
echo "===== INTERNET PING ====="
|
||||
ping -c 4 1.1.1.1
|
||||
|
||||
echo
|
||||
echo "===== DNS TEST ====="
|
||||
dig google.com
|
||||
|
||||
echo
|
||||
echo "===== SITE DNS ====="
|
||||
dig leo-test.store
|
||||
|
||||
echo
|
||||
echo "===== HTTPS TEST ====="
|
||||
curl -Iv https://$1
|
||||
|
||||
echo
|
||||
echo "===== PORT TEST ====="
|
||||
nc -vz leo-test.store 443
|
||||
|
||||
echo
|
||||
echo "===== TRACEROUTE ====="
|
||||
traceroute 212.227.93.40
|
||||
|
||||
echo
|
||||
echo "===== OPENSSL ====="
|
||||
timeout 10 openssl s_client -connect $1:443 </dev/null
|
||||
|
||||
echo
|
||||
echo "===== JOURNAL WIFI ====="
|
||||
journalctl -b | grep -Ei 'wifi|wlan|wlp|networkmanager|dhcp' | tail -100
|
||||
|
||||
} > "$OUT" 2>&1
|
||||
|
||||
echo "Saved to $OUT"
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
PUB_IP=$(wget -T 5 -qO- ifconfig.co)
|
||||
printf "User: %s\n" "$(whoami)"
|
||||
if [ "${PUB_IP}" != "" ]; then
|
||||
LOC=$(wget -T 3 -qO- ifconfig.co/country)
|
||||
CITY=$(wget -T 3 -qO- ifconfig.co/city)
|
||||
printf "%s\t%s\t%s\n" "${PUB_IP}" "${LOC}" "${CITY}"
|
||||
else
|
||||
printf "No Connection\n"
|
||||
fi
|
||||
if [ "$(grep deluge-user /etc/passwd)" != "" ]; then
|
||||
printf "User: deluge-user\n"
|
||||
PUB_IP=$(sudo /home/lucas/scripts/deluge_user_cmd.sh wget -T 3 -qO- ifconfig.co)
|
||||
if [ "${PUB_IP}" == "" ]; then
|
||||
PUB_IP=$(sudo /home/lucas/scripts/deluge_user_cmd.sh curl --connect-timeout 3 https://ipinfo.io/ip)
|
||||
fi
|
||||
if [ "${PUB_IP}" != "" ]; then
|
||||
LOC=$(sudo /home/lucas/scripts/deluge_user_cmd.sh wget -T 3 -qO- ifconfig.co/country)
|
||||
if [ "${LOC}" == "" ]; then
|
||||
LOC=$(sudo /home/lucas/scripts/deluge_user_cmd.sh curl --connect-timeout 3 https://ipinfo.io/country 2>>/dev/null)
|
||||
fi
|
||||
CITY=$(sudo /home/lucas/scripts/deluge_user_cmd.sh wget -T 3 -qO- ifconfig.co/city)
|
||||
if [ "${CITY}" == "" ]; then
|
||||
CITY=$(sudo /home/lucas/scripts/deluge_user_cmd.sh curl --connect-timeout 3 https://ipinfo.io/city 2>>/dev/null)
|
||||
fi
|
||||
printf "%s\t%s\t%s\n" "${PUB_IP}" "${LOC}" "${CITY}"
|
||||
else
|
||||
printf "No Connection\n"
|
||||
fi
|
||||
fi
|
||||
Executable
+177
@@ -0,0 +1,177 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo Flush existing rules
|
||||
iptables -F
|
||||
iptables -F -t nat
|
||||
iptables -F -t mangle
|
||||
iptables -F -t filter
|
||||
echo Delete all
|
||||
iptables -X
|
||||
|
||||
DELUGE_MODE=false
|
||||
|
||||
if [ "$1" == "-d" ]; then
|
||||
echo Input -d open all traffic
|
||||
iptables -P INPUT ACCEPT
|
||||
iptables -P FORWARD ACCEPT
|
||||
exit 0
|
||||
elif [ "$1" == "-deluge-mode" ]; then
|
||||
echo "Iptables Deluge Mode"
|
||||
DELUGE_MODE=true
|
||||
else
|
||||
echo "Set up Firewall"
|
||||
fi
|
||||
|
||||
echo Set Variables
|
||||
HOST="$(hostname)"
|
||||
SSHPORT=$(cat /etc/ssh/sshd_config | grep Port | grep -v "#" | cut -f2 -d' ')
|
||||
|
||||
echo Make Rules
|
||||
|
||||
echo Drop incoming traffic and accept outgoing
|
||||
iptables -P INPUT DROP
|
||||
iptables -P FORWARD DROP
|
||||
iptables -P OUTPUT ACCEPT
|
||||
|
||||
echo "Established Connections"
|
||||
iptables -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
||||
iptables -A OUTPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
||||
echo "DHCP"
|
||||
iptables -A OUTPUT -p udp --dport 67 --sport 68 -m state --state NEW,ESTABLISHED -j ACCEPT
|
||||
iptables -A INPUT -p udp --sport 67 --dport 68 -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
if [ "${HOST}" == "quiethost" ]; then
|
||||
SD_PORT=7860
|
||||
echo Incoming SD Port ${SD_PORT}
|
||||
iptables -A INPUT -p tcp --dport ${SD_PORT} -m state --state NEW,ESTABLISHED -j ACCEPT
|
||||
iptables -A OUTPUT -p tcp --sport ${SD_PORT} -m state --state ESTABLISHED -j ACCEPT
|
||||
echo Incoming SD Port 9170
|
||||
iptables -A INPUT -p tcp --dport 9170 -m state --state NEW,ESTABLISHED -j ACCEPT
|
||||
iptables -A OUTPUT -p tcp --sport 9170 -m state --state ESTABLISHED -j ACCEPT
|
||||
elif [ "${HOST}" == "serverhost" ]; then
|
||||
DELUGE_PORT=58846
|
||||
echo Incoming https Port 8123
|
||||
iptables -A INPUT -p tcp --dport 8123 -m state --state NEW,ESTABLISHED -j ACCEPT
|
||||
iptables -A OUTPUT -p tcp --sport 8123 -m state --state ESTABLISHED -j ACCEPT
|
||||
echo Incoming https Port 80
|
||||
iptables -A INPUT -p tcp --dport 80 -m state --state NEW,ESTABLISHED -j ACCEPT
|
||||
iptables -A OUTPUT -p tcp --sport 80 -m state --state ESTABLISHED -j ACCEPT
|
||||
echo Incoming https Port 443
|
||||
iptables -A INPUT -p tcp --dport 443 -m state --state NEW,ESTABLISHED -j ACCEPT
|
||||
iptables -A OUTPUT -p tcp --sport 443 -m state --state ESTABLISHED -j ACCEPT
|
||||
echo Incoming https Port 444
|
||||
iptables -A INPUT -p tcp --dport 444 -m state --state NEW,ESTABLISHED -j ACCEPT
|
||||
iptables -A OUTPUT -p tcp --sport 444 -m state --state ESTABLISHED -j ACCEPT
|
||||
echo Deluge Daemon
|
||||
iptables -A INPUT -p tcp --dport ${DELUGE_PORT} -m state --state NEW,ESTABLISHED -j ACCEPT
|
||||
iptables -A OUTPUT -p tcp --sport ${DELUGE_PORT} -m state --state ESTABLISHED -j ACCEPT
|
||||
iptables -A INPUT -p tcp --dport 8096 -j ACCEPT
|
||||
iptables -A INPUT -p udp --dport 8096 -j ACCEPT
|
||||
iptables -A INPUT -p tcp --dport 4533 -j ACCEPT
|
||||
iptables -A INPUT -p udp --dport 4533 -j ACCEPT
|
||||
iptables -A INPUT -p tcp --dport 13378 -j ACCEPT
|
||||
iptables -A INPUT -p udp --dport 13378 -j ACCEPT
|
||||
fi
|
||||
echo "Incoming SSH -P $SSHPORT"
|
||||
iptables -A INPUT -p tcp -m state --state NEW -m tcp --dport $SSHPORT -j ACCEPT
|
||||
iptables -A OUTPUT -p tcp --sport $SSHPORT -m state --state ESTABLISHED -j ACCEPT
|
||||
echo "Incoming XRDP Port"
|
||||
iptables -A INPUT -p tcp -m state --state NEW -m tcp --dport 3389 -j ACCEPT
|
||||
iptables -A OUTPUT -p tcp --sport 3389 -m state --state ESTABLISHED -j ACCEPT
|
||||
echo "Ping Reply"
|
||||
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
|
||||
iptables -A OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT
|
||||
echo "Send Mail"
|
||||
iptables -A INPUT -p 25 -j ACCEPT
|
||||
iptables -A OUTPUT -p 25 -j ACCEPT
|
||||
echo "loopback"
|
||||
iptables -A INPUT -i lo -j ACCEPT
|
||||
iptables -A OUTPUT -o lo -j ACCEPT
|
||||
if [[ "$(grep -E "no-internet:" /etc/group)" == "" ]]; then
|
||||
echo "Add no-internet group"
|
||||
sudo addgroup no-internet
|
||||
fi
|
||||
echo "Add No Internet Group Rule"
|
||||
sudo iptables -A OUTPUT -m owner --gid-owner no-internet -j DROP
|
||||
# On Fail:
|
||||
# sudo addgroup no-internet
|
||||
# sudo adduser lucas no-internet
|
||||
|
||||
|
||||
if [ "${HOST}" == "raspberry" ]; then
|
||||
iptables -A FORWARD -i ${NETIF} -o wlan0 -j ACCEPT
|
||||
iptables -A FORWARD -i wlan0 -o ${NETIF} -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
sysctl -w net.ipv4.ip_forward=1
|
||||
iptables -t nat -A POSTROUTING -o wlan0 -j MASQUERADE
|
||||
fi
|
||||
if [ "$(cat /etc/passwd | grep -E "deluge-user:x:[0-9]{4}:[0-9]{4}")" != "" ]; then
|
||||
echo "deluge-user Settings"
|
||||
if [ "${VPNUSER}" == "" ]; then
|
||||
VPNUSER="deluge-user"
|
||||
fi
|
||||
if [ "${LOCALIP}" == "" ]; then
|
||||
NETIF="$(lshw -class network | grep -A6 "Ethernet interface" | grep name | awk -F': ' '{print $2}')"
|
||||
LOCALIP=$(ip addr show ${NETIF} | grep -Po '(?<= inet )([0-9\.]+)')
|
||||
fi
|
||||
if [ "${MARK_ID}" == "" ]; then
|
||||
MARK_ID="0x32"
|
||||
fi
|
||||
|
||||
if [[ $(ip rule list | grep -c $MARK_ID) == 0 ]]; then
|
||||
ip rule add from all fwmark $MARK_ID lookup $VPNUSER
|
||||
fi
|
||||
|
||||
echo Interface $NETIF
|
||||
echo IP: $LOCALIP
|
||||
echo "mark packets from ${VPNUSER} to ${LOCALIP} with ${MARK_ID}"
|
||||
iptables -t mangle -A OUTPUT -j CONNMARK --restore-mark
|
||||
iptables -t mangle -A OUTPUT ! --dest ${LOCALIP} -m owner --uid-owner ${VPNUSER} -j MARK --set-mark ${MARK_ID}
|
||||
iptables -t mangle -A OUTPUT --dest ${LOCALIP} -p udp --dport 53 -m owner --uid-owner ${VPNUSER} -j MARK --set-mark ${MARK_ID}
|
||||
iptables -t mangle -A OUTPUT --dest ${LOCALIP} -p tcp --dport 53 -m owner --uid-owner ${VPNUSER} -j MARK --set-mark ${MARK_ID}
|
||||
iptables -t mangle -A OUTPUT ! --src ${LOCALIP} -j MARK --set-mark ${MARK_ID}
|
||||
iptables -t mangle -A OUTPUT -j CONNMARK --save-mark
|
||||
|
||||
echo "Set DNS for ${VPNUSER}"
|
||||
iptables -t nat -A OUTPUT --dest ${LOCALIP} -p udp --dport 53 -m owner --uid-owner ${VPNUSER} -j DNAT --to-destination 8.8.4.4
|
||||
iptables -t nat -A OUTPUT --dest ${LOCALIP} -p tcp --dport 53 -m owner --uid-owner ${VPNUSER} -j DNAT --to-destination 8.8.8.8
|
||||
|
||||
echo "let ${VPNUSER} access lo"
|
||||
iptables -A OUTPUT -o lo -m owner --uid-owner ${VPNUSER} -j ACCEPT
|
||||
|
||||
iptables -A INPUT -i ${NETIF} -s 192.168.0.0/24 -p udp --dport 137:138 -m state --state NEW,ESTABLISHED -j ACCEPT
|
||||
iptables -A OUTPUT -o ${NETIF} -d 192.168.0.0/24 -p udp --sport 137:138 -m state --state ESTABLISHED -j ACCEPT
|
||||
iptables -A INPUT -i ${NETIF} -s 192.168.0.0/24 -p tcp --dport 139 -m state --state NEW,ESTABLISHED -j ACCEPT
|
||||
iptables -A OUTPUT -o ${NETIF} -d 192.168.0.0/24 -p tcp --sport 139 -m state --state ESTABLISHED -j ACCEPT
|
||||
iptables -A INPUT -i ${NETIF} -s 192.168.0.0/24 -p tcp --dport 445 -m state --state NEW,ESTABLISHED -j ACCEPT
|
||||
# qbittorrent
|
||||
iptables -A INPUT -p tcp --dport 8080 -j ACCEPT
|
||||
# nfs to radarr/sonarr
|
||||
iptables -A INPUT -p tcp -s 192.168.0.4 --dport 2049 -j ACCEPT
|
||||
|
||||
if [ "${DELUGE_MODE}" == "true" ]; then
|
||||
printf "Local IP: ${LOCALIP}\nInterface: $NETIF\nVPN IP: $VPNIP\nVPN Interface: $VPNIF\nP2P IP: $P2PIP\nVPN User: ${VPNUSER}\nMarkID: ${MARK_ID}\n"
|
||||
echo "Deluge and VPN Settings"
|
||||
|
||||
echo Established VPN connections
|
||||
iptables -A INPUT -i $VPNIF -m conntrack --ctstate ESTABLISHED -j ACCEPT
|
||||
|
||||
echo "block everything incoming on $VPNIF"
|
||||
iptables -A INPUT -i $VPNIF -j REJECT
|
||||
|
||||
echo "let ${VPNUSER} access $VPNIF"
|
||||
iptables -A OUTPUT -o $VPNIF -m owner --uid-owner ${VPNUSER} -j ACCEPT
|
||||
|
||||
echo all packets on $VPNIF needs to be masqueraded
|
||||
iptables -t nat -A POSTROUTING -o $VPNIF -j MASQUERADE
|
||||
|
||||
echo "reject connections from predator IP going over $NETIF"
|
||||
iptables -A OUTPUT ! --src ${LOCALIP} -o $NETIF -j REJECT
|
||||
fi
|
||||
fi
|
||||
LOG=false
|
||||
if $LOG; then
|
||||
echo Logging
|
||||
iptables -N LOGGING
|
||||
iptables -A INPUT -j LOGGING
|
||||
iptables -A LOGGING -m limit --limit 2/min -j LOG --log-prefix "IPTables Packet Dropped: " --log-level 7
|
||||
iptables -A LOGGING -j DROP
|
||||
fi
|
||||
Executable
+112
@@ -0,0 +1,112 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo Flush existing rules
|
||||
iptables -F
|
||||
iptables -F -t nat
|
||||
iptables -F -t mangle
|
||||
iptables -F -t filter
|
||||
echo Delete all
|
||||
iptables -X
|
||||
|
||||
|
||||
if [ "$1" == "-d" ]; then
|
||||
echo Input -d open all traffic
|
||||
iptables -P INPUT ACCEPT
|
||||
iptables -P FORWARD ACCEPT
|
||||
exit 0
|
||||
else
|
||||
echo "Standard Firewall, -d to open all"
|
||||
fi
|
||||
ifx=( $(/sbin/ifconfig -s -a | tail -n +2 | cut -f1 -d' ' | grep -v lo) )
|
||||
for i in ${!ifx[@]}; do
|
||||
ipx[$i]="$(/sbin/ifconfig ${ifx[$i]} | grep -m1 -o -P '(?<=inet ).*(?= netmask)')"
|
||||
if [ "${ipx[$i]}" == "" ]; then
|
||||
ipx[$i]="----------"
|
||||
fi
|
||||
printf "%s:\t%s\n" "${ifx[$i]}" "${ipx[$i]}"
|
||||
done
|
||||
|
||||
echo Set Variables
|
||||
HOST=$(hostname)
|
||||
SSHPORT=$(cat /etc/ssh/sshd_config | grep Port | grep -v "#" | cut -f2 -d' ')
|
||||
printf "Local IP: $LOCALIP\nInterface: $NETIF\n"
|
||||
|
||||
echo Make Rules
|
||||
|
||||
|
||||
echo Allen verkehr blockieren
|
||||
iptables -P INPUT DROP
|
||||
iptables -P FORWARD DROP
|
||||
iptables -P OUTPUT ACCEPT
|
||||
|
||||
for i in ${!ifx[@]}; do
|
||||
LOCALIP="${ipx[$i]}"
|
||||
NETIF="${ifx[$i]}"
|
||||
echo "Established Connections for $NETIF"
|
||||
iptables -A INPUT -i $NETIF -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
||||
iptables -A OUTPUT -o $NETIF -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
||||
echo "DHCP for $NETIF"
|
||||
iptables -A OUTPUT -o $NETIF -p udp --dport 67 --sport 68 -m state --state NEW,ESTABLISHED -j ACCEPT
|
||||
iptables -A INPUT -i $NETIF -p udp --sport 67 --dport 68 -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
echo "reject connections from predator IP going over $NETIF"
|
||||
iptables -A OUTPUT ! --src $LOCALIP -o $NETIF -j REJECT
|
||||
if [ "${HOST}" == "serverhost" ]; then
|
||||
echo Incoming https
|
||||
iptables -A INPUT -i $NETIF -p tcp --dport 443 -m state --state NEW,ESTABLISHED -j ACCEPT
|
||||
iptables -A OUTPUT -o $NETIF -p tcp --sport 443 -m state --state ESTABLISHED -j ACCEPT
|
||||
fi
|
||||
echo "Incoming SSH P3032 for $NETIF"
|
||||
iptables -A INPUT -i $NETIF -p tcp -m state --state NEW -m tcp --dport $SSHPORT -j ACCEPT
|
||||
iptables -A OUTPUT -o $NETIF -p tcp --sport $SSHPORT -m state --state ESTABLISHED -j ACCEPT
|
||||
echo "Ping Reply for $NETIF"
|
||||
iptables -A INPUT -i $NETIF -p icmp --icmp-type echo-request -j ACCEPT
|
||||
iptables -A OUTPUT -o $NETIF -p icmp --icmp-type echo-reply -j ACCEPT
|
||||
done
|
||||
echo loopback
|
||||
iptables -A INPUT -i lo -j ACCEPT
|
||||
iptables -A OUTPUT -o lo -j ACCEPT
|
||||
|
||||
echo Logging
|
||||
iptables -N LOGGING
|
||||
iptables -A INPUT -j LOGGING
|
||||
iptables -A LOGGING -m limit --limit 2/min -j LOG --log-prefix "IPTables Packet Dropped: " --log-level 7
|
||||
iptables -A LOGGING -j DROP
|
||||
|
||||
#echo Outgoing https
|
||||
#iptables -A OUTPUT -o $NETIF -p tcp --dport 443 -m state --state NEW,ESTABLISHED -j ACCEPT
|
||||
#iptables -A INPUT -i $NETIF -p tcp --sport 443 -m state --state ESTABLISHED -j ACCEPT
|
||||
|
||||
#echo torrents
|
||||
#iptables -A OUTPUT -o $NETIF -p udp -m state --state NEW,ESTABLISHED -j ACCEPT
|
||||
#iptables -A INPUT -i $NETIF -p tcp --sport 6890:6999 -m state --state NEW,ESTABLISHED -j ACCEPT
|
||||
#iptables -A INPUT -i $NETIF -p udp --sport 6890:6999 -m state --state NEW,ESTABLISHED -j ACCEPT
|
||||
|
||||
#echo Outgoing SSH p1160
|
||||
#iptables -A INPUT -i $NETIF -p tcp --sport 1160 -m state --state ESTABLISHED -j ACCEPT
|
||||
#iptables -A OUTPUT -o $NETIF -p tcp --dport 1160 -m state --state NEW,ESTABLISHED -j ACCEPT
|
||||
|
||||
#echo Ping Request
|
||||
#iptables -A OUTPUT -o $NETIF -p icmp --icmp-type echo-request -j ACCEPT
|
||||
#iptables -A INPUT -i $NETIF -p icmp --icmp-type echo-reply -j ACCEPT
|
||||
|
||||
#echo Incoming SSH p22
|
||||
#iptables -A INPUT -i $NETIF -p tcp --dport 22 -m state --state NEW,ESTABLISHED -j ACCEPT
|
||||
#iptables -A OUTPUT -o $NETIF -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
|
||||
|
||||
#echo Outgoing SSH p22
|
||||
#iptables -A INPUT -i $NETIF -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
|
||||
#iptables -A OUTPUT -o $NETIF -p tcp --dport 22 -m state --state NEW,ESTABLISHED -j ACCEPT
|
||||
|
||||
#echo outgouing http
|
||||
#iptables -A OUTPUT -o $NETIF -p tcp --dport 80 -m state --state NEW -j ACCEPT
|
||||
#iptables -A INPUT -i $NETIF -p tcp --sport 80 -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
|
||||
#echo incoming http
|
||||
#iptables -A INPUT -i $NETIF -p tcp --dport 80 -m state --state RELATED,ESTABLISHED -j DROP
|
||||
#iptables -A OUTPUT -o $NETIF -p tcp --sport 80 -m state --state NEW -j DROP
|
||||
|
||||
#echo DNS
|
||||
#iptables -A OUTPUT -o $NETIF -p tcp --dport 53 -m state --state NEW -j ACCEPT
|
||||
#iptables -A OUTPUT -o $NETIF -p udp --dport 53 -m state --state NEW -j ACCEPT
|
||||
#iptables -A INPUT -i $NETIF -p tcp --sport 53 -m state --state NEW -j ACCEPT
|
||||
#iptables -A INPUT -i $NETIF -p udp --sport 53 -m state --state NEW -j ACCEPT
|
||||
Executable
+435
@@ -0,0 +1,435 @@
|
||||
#!/bin/bash
|
||||
green="\e[32m"
|
||||
red="\e[31m"
|
||||
blue="\e[34m"
|
||||
norm="\e[0m"
|
||||
#IFS='\n'
|
||||
hostname="$(hostname)"
|
||||
export LOG_DIR="/home/lucas/.LOG"
|
||||
export VPN_DIR="/home/lucas/Documents/Dokumente/Projekte/Server/VPN/"
|
||||
export SCRIPTS_DIR="/home/lucas/Documents/Dokumente/Projekte/Server/scripts"
|
||||
ls ${SCRIPTS_DIR} >>/dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
export VPN_DIR="/home/lucas/Documents/Dokumente/Projekte/Server/VPN/"
|
||||
export SCRIPTS_DIR="/home/lucas/Documents/Dokumente/Projekte/Server/scripts"
|
||||
ls ${SCRIPTS_DIR} >>/dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Script Dir Not present"
|
||||
exit
|
||||
fi
|
||||
fi
|
||||
|
||||
date=$(date +"%m%d%H%M%S")
|
||||
SINGLE=false
|
||||
EXPRESSVPN=false
|
||||
SHARKVPN=true
|
||||
LOG=false
|
||||
DELUGE_MODE=false
|
||||
GATEWAY_MODE=false
|
||||
NORMAL_MODE=false;
|
||||
QUIT=false;
|
||||
LIST=false
|
||||
usage(){
|
||||
printf "Usage:
|
||||
-n\t\tNormal Mode
|
||||
-del\t\tDeluge Mode
|
||||
-list\t\tList ovpn Files
|
||||
-all\t\tUse all Files(FreeVPN)
|
||||
-std\t\tTry a default vpn first
|
||||
-gw\t\tGateway Mode(Raspberry)
|
||||
-q\t\tQuit VPN
|
||||
-l\t\tLog Output
|
||||
-s <param>\tSearch Mode
|
||||
-tu\t\tConnect to TU Wien VPN
|
||||
-tud\t\tDisconnect from TU Wien VPN
|
||||
-h\t\tHelp\n"
|
||||
}
|
||||
|
||||
display_ip() {
|
||||
if [ "$1" == "duser" ]; then
|
||||
OBJ_duser_cmd="sudo -u deluge-user -i -- "
|
||||
else
|
||||
OBJ_duser_cmd=""
|
||||
fi
|
||||
echo whoami
|
||||
OBJ_USER=$(${OBJ_duser_cmd}whoami)
|
||||
OBJ_PUB_COUNTRY="Austria"
|
||||
count=0
|
||||
while true; do
|
||||
sleep 1
|
||||
OBJ_PUBIP=$(${OBJ_duser_cmd}wget -T 10 -qO- ifconfig.co)
|
||||
OBJ_PUB_COUNTRY=$(${OBJ_duser_cmd}wget -T 10 -qO- ifconfig.co/country)
|
||||
if [[ "${DELUGE_MODE}" == "true" ]] || [[ "${OBJ_PUB_COUNTRY}" != "Austria" ]] && [[ "${OBJ_PUB_COUNTRY}" != "" ]] || [[ "${OBJ_PUBIP}" != "${STDIP}" ]] && [[ "${OBJ_PUBIP}" != "" ]]; then
|
||||
break
|
||||
fi
|
||||
printf ". "
|
||||
if [ ${count} -gt 20 ]; then
|
||||
echo "Timeout... VPN Failed"
|
||||
break
|
||||
fi
|
||||
((count++))
|
||||
done
|
||||
printf "User: ${OBJ_USER}"
|
||||
for ((i=${#OBJ_USER};i<20;i++)); do
|
||||
printf " "
|
||||
done
|
||||
printf "IP: $OBJ_PUBIP\tCountry: $OBJ_PUB_COUNTRY\n"
|
||||
}
|
||||
connect_tuwien() {
|
||||
if [ "$(pgrep openvpn)" != "" ]; then
|
||||
printf "VPN already existing --- Killed\n"
|
||||
kill $(pgrep openvpn)
|
||||
sleep 1
|
||||
pgrep openvpn
|
||||
fi
|
||||
printf "VPN \t\t\t\t[${green}DOWN${norm}]\n"
|
||||
if [ $(/opt/cisco/anyconnect/bin/vpn state | grep Disconnected | wc -l) -gt 0 ]; then
|
||||
sudo -u lucas /opt/cisco/anyconnect/bin/vpn -s < /home/lucas/.vpn_creds connect vpn.tuwien.ac.at
|
||||
echo "Disconnect with vpn -tud"
|
||||
else
|
||||
echo "TU VPN not disconnected"
|
||||
fi
|
||||
}
|
||||
disconnect_tuwien() {
|
||||
/opt/cisco/anyconnect/bin/vpn disconnect
|
||||
/home/lucas/Documents/Dokumente/Projekte/Server/scripts/network/networksettings.sh -std
|
||||
}
|
||||
get_config() {
|
||||
while [[ ${1:0:1} == - ]]; do
|
||||
echo "Param: $1"
|
||||
[[ $1 == -n ]] && {
|
||||
NORMAL_MODE=true;
|
||||
shift 1;
|
||||
continue;
|
||||
};
|
||||
[[ $1 == -del ]] && {
|
||||
DELUGE_MODE=true;
|
||||
shift 1;
|
||||
continue;
|
||||
};
|
||||
[[ $1 == -std ]] && {
|
||||
SINGLE=true
|
||||
SEARCH="austria"
|
||||
shift 1;
|
||||
continue;
|
||||
};
|
||||
[[ $1 == -list ]] && {
|
||||
LIST=true;
|
||||
shift 1;
|
||||
continue;
|
||||
};
|
||||
[[ $1 == -all ]] && {
|
||||
EXPRESSVPN=false;
|
||||
shift 1;
|
||||
continue;
|
||||
};
|
||||
[[ $1 == -gw ]] && {
|
||||
GATEWAY_MODE=true;
|
||||
shift 1;
|
||||
continue;
|
||||
};
|
||||
[[ $1 == -q ]] && {
|
||||
QUIT=true;
|
||||
shift 1;
|
||||
continue;
|
||||
};
|
||||
[[ $1 == -l ]] && {
|
||||
LOG=true;
|
||||
shift 1;
|
||||
continue;
|
||||
};
|
||||
[[ $1 == -h ]] && {
|
||||
usage;
|
||||
exit;
|
||||
};
|
||||
[[ $1 == -tu ]] && {
|
||||
connect_tuwien;
|
||||
exit;
|
||||
};
|
||||
[[ $1 == -tud ]] && {
|
||||
disconnect_tuwien;
|
||||
exit;
|
||||
};
|
||||
[[ $1 == -s ]] && {
|
||||
if [ "$2" == "" ]; then
|
||||
echo "Search Parameter Missing"
|
||||
usage;
|
||||
exit;
|
||||
fi
|
||||
SINGLE=true;
|
||||
SEARCH="$2"
|
||||
shift 2;
|
||||
continue;
|
||||
};
|
||||
echo "Parameter $1 wrong"
|
||||
usage
|
||||
exit 1;
|
||||
done
|
||||
}
|
||||
|
||||
connect_vpn() {
|
||||
export STDIP=$(wget -T 10 -qO- ifconfig.co)
|
||||
export NETIF="$(lshw -class network | grep -A6 "Ethernet interface" | grep name | awk -F': ' '{print $2}')"
|
||||
if [ "${hostname}" == "raspberry" ]; then
|
||||
export NETIF="$(lshw -class network | grep -A6 "Wireless interface" | grep name | awk -F': ' '{print $2}')"
|
||||
fi
|
||||
find ${LOG_DIR}/log_* -mtime +1 -type f -delete
|
||||
1>${LOG_DIR}/log_${hostname}_$date.log
|
||||
IFS=$'\n'; set -f
|
||||
if $EXPRESSVPN; then
|
||||
SEARCH_EXPRESS="my_expressvpn_"
|
||||
elif $SHARKVPN; then
|
||||
SEARCH_EXPRESS="surfshark"
|
||||
else
|
||||
SEARCH_EXPRESS=""
|
||||
fi
|
||||
if $SINGLE; then
|
||||
VPN_arr=($(find ${VPN_DIR} -name '*.ovpn' | grep "${SEARCH_EXPRESS}" | grep -i "${SEARCH}"))
|
||||
filename="${VPN_arr[0]}"
|
||||
else
|
||||
VPN_arr=($(find ${VPN_DIR} -name '*.ovpn' | grep "${SEARCH_EXPRESS}"))
|
||||
NUM=$(($RANDOM % ${#VPN_arr[@]}))
|
||||
filename="${VPN_arr[${NUM}]}"
|
||||
fi
|
||||
unset IFS; set +f
|
||||
export PUBIP=($(dig +short $(cat $filename | grep "remote " | cut -d' ' -f2)))
|
||||
if [ "$(pgrep openvpn)" != "" ]; then
|
||||
printf "VPN already existing --- Killed\n"
|
||||
kill $(pgrep openvpn)
|
||||
sleep 1
|
||||
pgrep openvpn
|
||||
fi
|
||||
printf "VPN \t\t\t\t[${green}DOWN${norm}]\n"
|
||||
|
||||
while [ -z ${VPNIF} ] && [ ${#VPN_arr[@]} -ge 1 ] ; do
|
||||
printf "OpenVPN: Connect to: $blue$( echo $filename | rev | cut -f1 -d'/' | rev)${norm}, ${NUM} of ${#VPN_arr[@]} VPNs\n"
|
||||
if $DELUGE_MODE; then
|
||||
filename_buffer="$(echo ${filename} | sed 's/\.ovpn/BUFF.ovpn/g')"
|
||||
if $EXPRESSVPN; then
|
||||
cat ${filename} | grep -v 'pull' | sed 's/dev tun/client\ndev tun/g' | sed 's/<cert>/route-nopull\nscript-security 2\ndown '$(echo ${SCRIPTS_DIR} | sed 's/\//\\\//g')'\/network\/rev_routing.sh\n<cert>/g' > ${filename_buffer}
|
||||
elif $SHARKVPN; then
|
||||
cat ${filename} | grep -v 'pull' | sed 's/<ca>/route-nopull\nscript-security 2\ndown '$(echo ${SCRIPTS_DIR} | sed 's/\//\\\//g')'\/network\/rev_routing.sh\n<ca>/g' > ${filename_buffer}
|
||||
fi
|
||||
filename=${filename_buffer}
|
||||
fi
|
||||
if ${LOG}; then
|
||||
exec 3>&1 4>&2 1>>${LOG_DIR}/log_${hostname}_$date.log 2>&1
|
||||
fi
|
||||
echo "${SCRIPTS_DIR}/network/openvpn.sh $filename"
|
||||
bash ${SCRIPTS_DIR}/network/openvpn.sh $filename
|
||||
# if ${LOG}; then
|
||||
# exec 1>&3 2>&4
|
||||
# fi
|
||||
|
||||
printf "Load tun interface "
|
||||
count=0
|
||||
while [ -z ${VPNIF} ] && [ -z ${VPNIP} ] && [ -z ${P2PIP} ]; do
|
||||
sleep 1
|
||||
export VPNIF="$(ip -br link | cut -f1 -d' ' | grep -o tun[0-9])"
|
||||
if [ ! -z ${VPNIF} ]; then
|
||||
export VPNIP=$(ip addr show ${VPNIF} | grep -Po '(?<=inet )([0-9\.]+)')
|
||||
export P2PIP=$(ip addr show ${VPNIF} | grep -Po '(?<=peer )([0-9\.]+)')
|
||||
export P2PIP=$VPNIP
|
||||
fi
|
||||
printf ". "
|
||||
if [ ${count} -gt 20 ]; then
|
||||
echo "Timeout... VPN Failed"
|
||||
break
|
||||
fi
|
||||
((count++))
|
||||
done
|
||||
echo ""
|
||||
echo "VPN IF: ${VPNIF}, VPN IP: ${VPNIP}, P2P IP: ${P2PIP}"
|
||||
export MARK_ID="0x32"
|
||||
export VPNIF="$(ip -br link | cut -f1 -d' ' | grep -o tun[0-9])"
|
||||
export VPNUSER="deluge-user"
|
||||
export LOCALIP=$(ip addr show ${NETIF} | grep -Po '(?<= inet )([0-9\.]+)')
|
||||
if [ ! -z ${VPNIF} ]; then
|
||||
export VPNIP=$(ip addr show ${VPNIF} | grep -Po '(?<=inet )([0-9\.]+)')
|
||||
export P2PIP=$(ip addr show ${VPNIF} | grep -Po '(?<=peer )([0-9\.]+)')
|
||||
export P2PIP=$VPNIP
|
||||
fi
|
||||
|
||||
|
||||
if [ -z ${VPNIF} ] || [ -z ${VPNIP} ]; then
|
||||
if ${LOG}; then
|
||||
exec 1>&3 2>&4
|
||||
fi
|
||||
echo "VPN IF: ${VPNIF}, VPN IP: ${VPNIP}, P2P IP: ${P2PIP}"
|
||||
printf "VPN\t\t\t\t[${red}FAIL${norm}]\n"
|
||||
unset VPN_arr[${NUM}]
|
||||
VPN_arr=("${VPN_arr[@]}")
|
||||
if [ ${#VPN_arr[@]} -lt 1 ]; then break; fi
|
||||
NUM=$(($RANDOM % ${#VPN_arr[@]}))
|
||||
filename="${VPN_arr[$NUM]}"
|
||||
echo "VPN arr size: ${#VPN_arr[@]}, NUM: $NUM, filename: $filename"
|
||||
pkill openvpn
|
||||
fi
|
||||
if ${DELUGE_MODE}; then
|
||||
rm ${filename_buffer}
|
||||
if [ "$(ls $(echo ${filename_buffer} | rev | cut -f2- -d'/' | rev) | grep "BUFF\.ovpn")" != "" ]; then
|
||||
rm $(echo ${filename_buffer} | rev | cut -f2- -d'/' | rev)/*BUFF.ovpn
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z ${VPNIF} ] || [ -z ${VPNIP} ] ; then
|
||||
printf "VPN\t\t\t\t[${red}FAIL${norm}]\n"
|
||||
printf "VPN Script failed: IF: ${VPNIF}\n" >>${LOG_DIR}/log_${hostname}_$date.log
|
||||
pkill openvpn
|
||||
exit 0
|
||||
fi
|
||||
|
||||
printf "VPN\t\t\t\t[${green}OK${norm}]\n"
|
||||
|
||||
if ${DELUGE_MODE}; then
|
||||
printf "${LOCALIP} at ${NETIF}\n" >>${LOG_DIR}/log_${hostname}_$date.log
|
||||
printf "${VPNIP} at VPN IP: ${VPNIF} and P2P IP: ${P2PIP}, User: $VPNUSER, Mark ID: $MARK_ID\n" >>${LOG_DIR}/log_${hostname}_$date.log
|
||||
for PUB_IP_ADDR in ${PUBIP[@]}; do
|
||||
echo "Public VPN IP: ${PUB_IP_ADDR}" >>${LOG_DIR}/log_${hostname}_$date.log
|
||||
done
|
||||
printf "${VPNIF}: ${VPNIP}\n";
|
||||
if ${LOG}; then
|
||||
exec 3>&1 4>&2 1>>${LOG_DIR}/log_${hostname}_$date.log 2>&1
|
||||
fi
|
||||
printf "Execute IPTables.sh\n"
|
||||
bash ${SCRIPTS_DIR}/network/iptables_basic.sh -deluge-mode #>>${LOG_DIR}/log_${hostname}_$date.log
|
||||
printf "Execute routing.sh\n"
|
||||
bash ${SCRIPTS_DIR}/network/routing.sh #>>${LOG_DIR}/log_${hostname}_$date.log
|
||||
printf "Modified IP Route List\n"
|
||||
ip route list
|
||||
printf "Modified ip route list table deluge-user\n"
|
||||
ip route list table deluge-user
|
||||
|
||||
printf "Set reverse path source validation to lose mode\n"
|
||||
sysctl_var=("net.ipv4.conf.all.rp_filter" "net.ipv4.conf.default.rp_filter" "net.ipv4.conf.${VPNIF}.rp_filter" "net.ipv4.conf.lo.rp_filter" "net.ipv4.conf.${NETIF}.rp_filter")
|
||||
for ((i=0;i<${#sysctl_var[@]};i++)); do
|
||||
echo $1: ${sysctl_var[$i]}, $(sysctl -n ${sysctl_var[$i]})
|
||||
if [ $(sysctl -n ${sysctl_var[$i]}) -ne 2 ]; then
|
||||
echo $i
|
||||
sysctl -w ${sysctl_var[$i]}=2
|
||||
fi
|
||||
done
|
||||
if ${LOG}; then
|
||||
exec 1>&3 2>&4
|
||||
fi
|
||||
printf "IPtables:\t\t\t[${green}OK${norm}]\n"
|
||||
printf "IProute:\t\t\t[${green}OK${norm}]\n"
|
||||
printf "RPFilter:\t\t\t[${green}OK${norm}]\n"
|
||||
|
||||
if [ "$userIP" != "$duserIP" ] && [ "$duserIP" != "" ] ; then
|
||||
printf "VPN Settings:\t\t\t[${green}OK${norm}]\n$(whoami)IP: $userIP\nduserIP: $duserIP \n"
|
||||
fi
|
||||
|
||||
printf "deluged service:\t\t"
|
||||
if [ "$(ps -elf | grep deluged | grep 'deluge-+')" == "" ]; then
|
||||
sudo -u deluge-user -i -- deluged
|
||||
sleep 0.3
|
||||
if [ $(ip addr | grep "127.0.0.2" | wc -l) -eq 0 ]; then
|
||||
ip addr add 127.0.0.2 dev lo
|
||||
fi
|
||||
fi
|
||||
if [ "$(pgrep deluged)" != "" ]; then
|
||||
printf "[${blue}UP${norm}]\n"
|
||||
else
|
||||
printf "[${red}FAIL${norm}]\n"
|
||||
fi
|
||||
elif ${GATEWAY_MODE}; then
|
||||
SUBNETIF="eth0"
|
||||
sysctl -w net.ipv4.ip_forward=1
|
||||
iptables -t nat -A POSTROUTING -o ${VPNIF} -j MASQUERADE
|
||||
iptables -A FORWARD -i ${SUBNETIF} -o ${VPNIF} -j ACCEPT
|
||||
iptables -A FORWARD -i ${VPNIF} -o ${SUBNETIF} -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
iptables -D FORWARD -i eth0 -o wlan0 -j ACCEPT
|
||||
iptables -D FORWARD -i wlan0 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
printf "${VPNIP} at VPN IP: ${VPNIF} \nP2P IP: ${P2PIP}\nUser: $VPNUSER\n"
|
||||
for PUB_IP_ADDR in ${PUBIP[@]}; do
|
||||
echo "Public VPN IP: ${PUB_IP_ADDR}"
|
||||
done
|
||||
printf "${VPNIF}: ${VPNIP}\n";
|
||||
else
|
||||
printf "${VPNIP} at VPN IP: ${VPNIF}\nP2P IP: ${P2PIP}\nUser: $VPNUSER\n"
|
||||
for PUB_IP_ADDR in ${PUBIP[@]}; do
|
||||
echo "Public VPN IP: ${PUB_IP_ADDR}"
|
||||
done
|
||||
printf "${VPNIF}: ${VPNIP}\n";
|
||||
printf "IF: ${NETIF}\n"
|
||||
fi
|
||||
}
|
||||
|
||||
printf "Timestamp: $date\n"
|
||||
get_config "$@"
|
||||
if { [ "${NORMAL_MODE}" == "false" ] && [ "${DELUGE_MODE}" == "false" ] && [ "${GATEWAY_MODE}" == "false" ]; } || [ $# -eq 0 ]; then
|
||||
echo "No Mode Argument passed, choose by host"
|
||||
if [ "$(hostname)" == "quiethost" ]; then
|
||||
NORMAL_MODE=true
|
||||
fi
|
||||
if [ "$(hostname)" == "serverhost" ]; then
|
||||
DELUGE_MODE=true
|
||||
fi
|
||||
if [ "$(hostname)" == "raspberry" ]; then
|
||||
GATEWAY_MODE=true
|
||||
fi
|
||||
fi
|
||||
if ${QUIT}; then
|
||||
if [ "$(pgrep openvpn)" == "" ]; then
|
||||
printf "VPN not active\n"
|
||||
else
|
||||
echo "Disable VPN"
|
||||
pkill openvpn
|
||||
su - lucas -c "bash ${SCRIPTS_DIR}/profile_motd2.sh"
|
||||
fi
|
||||
display_ip
|
||||
exit 0
|
||||
fi
|
||||
if ${LIST}; then
|
||||
find ${VPN_DIR} -name '*.ovpn'
|
||||
exit
|
||||
fi
|
||||
if ${DELUGE_MODE} && [ "$(hostname)" != "serverhost" ]; then
|
||||
echo "Deluge Mode"
|
||||
printf "Hostname is %s, continue?(y/*)" "$(hostname)"
|
||||
read -t 10 -n 1 choice
|
||||
if [ "$choice" != "y" ]; then
|
||||
printf "\nAbort\n"
|
||||
exit
|
||||
fi
|
||||
elif ${DELUGE_MODE}; then
|
||||
printf "${blue}Deluge Mode${norm}\n"
|
||||
fi
|
||||
if ${GATEWAY_MODE} && [ "$(hostname)" != "raspberry" ]; then
|
||||
echo "Gateway Mode"
|
||||
printf "Hostname is %s, continue?(y/*)" "$(hostname)"
|
||||
read -t 10 -n 1 choice
|
||||
if [ "$choice" != "y" ]; then
|
||||
printf "\nAbort\n"
|
||||
exit
|
||||
fi
|
||||
elif ${GATEWAY_MODE}; then
|
||||
printf "${blue}Gateway Mode${norm}\n"
|
||||
fi
|
||||
if ${NORMAL_MODE}; then
|
||||
printf "${blue}Normal Mode${norm}\n"
|
||||
fi
|
||||
connect_vpn
|
||||
sleep 2
|
||||
display_ip
|
||||
|
||||
if ${DELUGE_MODE}; then
|
||||
display_ip "duser"
|
||||
if [[ "$(ps -elf | grep -v grep | grep noip)" == "" ]] ; then
|
||||
if [[ "$(service noip-duc status | grep 'active (running)')" == "" ]]; then
|
||||
#/usr/local/bin/noip2 &
|
||||
# Install noip service
|
||||
#
|
||||
echo "Install noip service"
|
||||
fi
|
||||
fi
|
||||
elif ${GATEWAY_MODE}; then
|
||||
if [ "$(hostname)" == "quiethost" ]; then
|
||||
bash "${SCRIPTS_DIR}/profile_motd2.sh"
|
||||
fi
|
||||
fi
|
||||
if [ "$(hostname)" == "quiethost" ]; then
|
||||
su - lucas -c "bash ${SCRIPTS_DIR}/quiethost/servertunnel.sh"
|
||||
fi
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
|
||||
OUT=/docker/monitoring/node_exporter/textfile_collector/custom.prom
|
||||
TS=$(date +%s%3N)
|
||||
DOCKER_CONTAINER_NUM=44
|
||||
{
|
||||
# Docker container states
|
||||
docker ps -a --format '{{.Names}}' | while read -r name; do
|
||||
if docker inspect -f '{{.State.Running}}' "$name" 2>/dev/null | grep -q true; then
|
||||
echo "docker_container_state{name=\"$name\"} 1"
|
||||
else
|
||||
echo "docker_container_state{name=\"$name\"} 0"
|
||||
fi
|
||||
done
|
||||
|
||||
echo docker_container_number $DOCKER_CONTAINER_NUM
|
||||
|
||||
# Public IP information
|
||||
IP_JSON=$(curl -fsSL http://ip-api.com/json)
|
||||
|
||||
IP=$(jq -r '.query' <<< "$IP_JSON")
|
||||
COUNTRY=$(jq -r '.country' <<< "$IP_JSON")
|
||||
CITY=$(jq -r '.city' <<< "$IP_JSON")
|
||||
ISP=$(jq -r '.isp' <<< "$IP_JSON" | sed 's/"/\\"/g')
|
||||
COUNTRYCODE=$(jq -r '.countryCode' <<< "$IP_JSON")
|
||||
LAT=$(jq -r '.lat' <<< "$IP_JSON")
|
||||
LON=$(jq -r '.lon' <<< "$IP_JSON")
|
||||
|
||||
echo "public_ip_info{ip=\"$IP\",country=\"$COUNTRY\",city=\"$CITY\",isp=\"$ISP\",countrycode=\"$COUNTRYCODE\",lat=\"$LAT\",lon=\"$LON\"} 1"
|
||||
echo "custom_metrics_last_update_timestamp_seconds $(date +%s)"
|
||||
} > "$OUT.$$"
|
||||
|
||||
mv "$OUT.$$" "$OUT"
|
||||
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ $1 == "" ]; then
|
||||
filename="${VPN_DIR}/my_expressvpn_usa_-_new_york_udp.ovpn"
|
||||
else
|
||||
filename=$1
|
||||
fi
|
||||
#echo $filename
|
||||
if [[ "$filname" == *"vpnbook"* ]]; then
|
||||
username="vpnbook"
|
||||
password="$(${SCRIPTS_DIR}/network/vpnpassword.sh vpnbook)"
|
||||
elif [[ $filename == *"FreeVPN"* ]]; then
|
||||
username="freevpn.$(echo $filename | cut -f2- -d'.' | cut -f1 -d'-')"
|
||||
password="$(${SCRIPTS_DIR}/network/vpnpassword.sh $username)"
|
||||
elif [[ "$filename" == *"expressvpn"* ]]; then
|
||||
username="x5yn6ytav3opb4ubjqvy13g5"
|
||||
password="joao7n67han31uwq8it6s3jb"
|
||||
elif [[ "$filename" == *"surfshark"* ]]; then
|
||||
username="RjzbvqjFFa9AzDrxvUTwrfPA"
|
||||
password="DtTnSbzS2tcWjScfGP6WZBXb"
|
||||
else
|
||||
echo "Wrong Input File"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ $password == "" ]; then
|
||||
exec 1>&3 2>&4
|
||||
echo "Get Password Failed: Password: $password"
|
||||
fi
|
||||
|
||||
echo "User: $username"
|
||||
echo "Password: $password"
|
||||
echo "Config File: $filename"
|
||||
expect <<EOD
|
||||
spawn sudo openvpn --config $filename
|
||||
expect "Username: "
|
||||
send "$username\r"
|
||||
expect "Password: "
|
||||
send "$password\r"
|
||||
expect EOF
|
||||
EOD
|
||||
exit 0
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#! /bin/bash
|
||||
LOGFILE="/home/lucas/Documents/Dokumente/Dokumente/Projekte/Server/VPN/LOG/log_down.log"
|
||||
> ${LOGFILE}
|
||||
echo $(id) >> ${LOGFILE}
|
||||
date >> ${LOGFILE}
|
||||
|
||||
if [ "$(cat /etc/iproute2/rt_tables | grep "deluge-user")" != "" ]; then
|
||||
if [ "$1" == "inet" ]; then
|
||||
printf "routing table for deluge-user for noral internet\n"
|
||||
ip route flush table deluge-user
|
||||
ip route add default via 10.0.0.138 dev eth0 proto static metric 100 table deluge-user
|
||||
ip route add 10.0.0.0/24 dev eth0 proto kernel scope link src 10.0.0.2 metric 100 table deluge-user
|
||||
else
|
||||
printf "routing table for deluge-user blocking all traffic\n" >> ${LOGFILE}
|
||||
ip route flush table deluge-user
|
||||
ip route replace default via 127.0.0.1 table deluge-user
|
||||
ip route flush cache
|
||||
fi
|
||||
elif [ "$(hostname)" == "raspberry" ]; then
|
||||
bash /home/lucas/Documents/Dokumente/Dokumente/Projekte/Server/scripts/iptables_basic.sh
|
||||
fi
|
||||
exit 0
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#! /bin/bash
|
||||
# Niftiest Software – www.niftiestsoftware.com
|
||||
# Modified version by HTPC Guides – www.htpcguides.com
|
||||
echo VPNUSER: $VPNUSER
|
||||
if [[ "$(grep $VPNUSER /etc/iproute2/rt_tables)" == "" ]]; then
|
||||
sudo sh -c 'echo "1 deluge-user" >> /etc/iproute2/rt_tables'
|
||||
fi
|
||||
if [ "$1" == "reverse" ]; then
|
||||
VPNUSER="$2"
|
||||
printf "reverse table for ${VPNUSER}\n"
|
||||
ip route flush table ${VPNUSER}
|
||||
ip route replace default via 127.0.0.1 table ${VPNUSER}
|
||||
ip route flush cache
|
||||
exit 0
|
||||
fi
|
||||
#${VPNIF}="tun+"
|
||||
#VPNUSER="deluge-user"
|
||||
#VPNIP="10.15.0.*"
|
||||
printf "${LOCALIP}\n${NETIF}\n${VPNIP}\n${VPNIF}\n${P2PIP}\n${VPNUSER}\n"
|
||||
printf "Flush old ${VPNUSER} table\n"
|
||||
ip route flush table ${VPNUSER}
|
||||
|
||||
#printf "Flush Main Table(Revert VPN Changes) and restore default\n"
|
||||
#ip route flush table main
|
||||
#ip route add 10.0.0.0/24 dev eth0 proto kernel scope link src 10.0.0.2 metric 100
|
||||
#ip route add 37.48.66.67 via 10.0.0.138 dev eth0 proto static metric 100
|
||||
#ip route add 169.254.0.0/16 dev eth0 proto static scope link metric 1000
|
||||
#ip route add default via 10.0.0.138 dev eth0 proto static metric 100
|
||||
|
||||
printf "Add rule for marked packets\n"
|
||||
if [[ $(ip rule list | grep -c ${MARK_ID}) == 0 ]]; then
|
||||
ip rule add from all fwmark ${MARK_ID} lookup ${VPNUSER}
|
||||
fi
|
||||
printf "Replace Default, Append Default, Flush cache\n"
|
||||
ip route replace default via ${P2PIP} table ${VPNUSER}
|
||||
#ip route append default via 127.0.0.1 dev lo table ${VPNUSER}
|
||||
for PUB_IP_ADDR in ${PUBIP[@]}; do
|
||||
echo "ip route add ${PUB_IP_ADDR}/32 via ${P2PIP} dev ${VPNIF} table ${VPNUSER}"
|
||||
ip route add ${PUB_IP_ADDR}/32 via ${P2PIP} dev ${VPNIF} table ${VPNUSER}
|
||||
done
|
||||
#ip route add 0.0.0.0/1 via ${VPNIP} dev ${VPNIF} table ${VPNUSER}
|
||||
#ip route add 128.0.0.0/1 via 10.15.0.125 table ${VPNUSER}
|
||||
#ip route add 10.15.0.1/32 via ${VPNIP} dev ${VPNIF} table ${VPNUSER}
|
||||
#ip route add ${P2PIP} dev ${VPNIF} proto kernel scope link src ${VPNIP} table ${VPNUSER}
|
||||
ip route flush cache
|
||||
|
||||
printf "run update-resolv-conf script to set VPN DNS\n"
|
||||
/etc/openvpn/update-resolv-conf
|
||||
|
||||
exit 0
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
ssh -o ExitOnForwardFailure=yes -fNL 127.0.0.2:58846:localhost:58846 \
|
||||
lucas@$(dig +short someoneslinux.ddns.net @resolver1.opendns.com) \
|
||||
-p 3032 \
|
||||
-i /home/lucas/.ssh/id_rsa
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!bin/bash
|
||||
printf "IP Rules:\n"
|
||||
ip rule
|
||||
printf "IP Routes:\n"
|
||||
ip route list
|
||||
printf "IP Routes for duser:\n"
|
||||
ip route list table deluge-user
|
||||
printf "IP Table:\n"
|
||||
sudo iptables -L
|
||||
@@ -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 = at-vie.prod.surfshark.com:51820
|
||||
@@ -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 = 6tOduT4iNbZyqKNzsSgDe/ckziO6101NPZwrnY5WpTk=
|
||||
AllowedIPs = 0.0.0.0/0
|
||||
Endpoint = bd-dac.prod.surfshark.com:51820
|
||||
@@ -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 = d3ldwEjnFcbLwD1o8uC5xC3DaSNek8DGeTpOb/h/IE4=
|
||||
AllowedIPs = 0.0.0.0/0
|
||||
Endpoint = de-ber.prod.surfshark.com:51820
|
||||
Loaded 100 of 271 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user