Compare commits

..
158 Commits
Author SHA1 Message Date
Lucas Vivo d06cff4cbd compare playlists m3u und navidrome 2026-10-12 16:33:53 +02:00
Lucas Vivo 09343a7523 first version of general audiobook library.py 2026-10-12 16:33:53 +02:00
Lucas Vivo dc0cd02e58 combine audiobooks: remove unecessary function 2026-10-12 16:33:53 +02:00
lucas 7158dd4960 change mqtt broker user to new mediahost impl 2026-10-12 12:07:28 +02:00
lucas 4c299c5a38 move sudo for upgrades into script and call script as user 2026-10-12 10:06:37 +00:00
Lucas Vivo 2f3a763054 adapt nextloud scan to docker 2026-10-09 17:36:49 +02:00
Lucas Vivo f7ff789d81 translate edit genre metadata to python
ads

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

asd
2026-10-08 12:26:08 +02:00
Lucas Vivo e4f1cce494 combine audiobook better logging 2026-10-07 18:59:18 +02:00
Lucas Vivo 8cccf674cf parallelize creat media function call 2026-10-07 18:36:39 +02:00
Lucas Vivo 86679e4e18 combine audiobooks: move temp dir 2026-10-07 14:58:16 +02:00
Lucas Vivo fc9a2dcf0e combine audiobooks: create audiobook in temp dir
add coloured skip message
2026-10-07 14:16:41 +02:00
Lucas Vivo 57520eaa04 update log print colours 2026-10-07 12:01:20 +02:00
Lucas Vivo f9ee0a0694 add percentage counter 2026-10-07 09:08:14 +02:00
Lucas Vivo 1af47022e9 move combine audiobooks script
chmod
uncomment delete lines
2026-10-07 06:58:25 +02:00
Lucas Vivo ce656b6970 add metadata copy to combine media script 2026-10-07 06:38:36 +02:00
lucas 881bdcd925 correct camera folder in rename camera script 2026-10-07 04:37:53 +00:00
lucas b4898bddfe set private tracker torrents to force seed 2026-10-06 21:13:06 +02:00
lucas 860100c4de backup calender/contacts script migration
move backup script to nextcloud folder
remove backup data
reconfigure calender/contacts backup
2026-10-06 21:13:06 +02:00
Lucas Vivo fb46bfcd3a move wol script 2026-10-06 21:11:19 +02:00
Lucas Vivo c68b09ffd1 combine mp3 m4a m4b audiobook files
combine split audiobook files to one m4b file
2026-10-06 21:11:19 +02:00
lucas 2ee909e6bb add qbittorrent version check to upgrade script 2026-10-06 13:45:11 +02:00
lucas 1d2c57dc02 add container number to node exporter 2026-10-06 11:44:40 +00:00
lucas 3514f3e489 add docker compose up to upgrade.sh 2026-10-04 19:42:37 +00:00
lucas 5b7cb93ebe docker daemon json file
docker metrics
mtu
/etc/docker/daemon.json
2026-10-04 14:13:38 +00:00
lucas b588773026 add node exporter textfile collector script
get ip adress and info
get docker container info
write to .prom file for prometheus

asd
2026-10-04 14:13:38 +00:00
Lucas Vivo ab80645034 working version of sonarr_translation
translate all episodes in root folder
only qurey present seasons
optimize code
2026-10-03 16:40:21 +02:00
Lucas Vivo 24b12ec6d6 sonarr transslation script
functional name and id query
get episodes from tmdb
match episodes
rename
2026-10-03 12:56:09 +02:00
Lucas Vivo a4da995ee1 ifx name filter in wol 2026-10-03 12:56:09 +02:00
lucas 39d8fb6cc5 add language param to generate torrent 2026-10-03 10:41:51 +02:00
lucas 8feec34575 repair torrent complete script 2026-10-02 19:29:17 +02:00
lucas e55b6ab2f2 add mytorrent cat to torrentcomplete
fix scp command
2026-10-02 14:38:59 +02:00
lucas f65e51509a add Screens and Poster and scp to generate torrent script 2026-10-02 14:38:36 +02:00
lucas e0a7968757 generate torrent script 2026-10-02 12:59:35 +02:00
lucas 77ac4654bb add docker prune to update script 2026-10-02 10:59:07 +00:00
lucas b4d3aa1ae5 add debug network connection script 2026-10-02 10:59:07 +00:00
lucas 6fe6a54e86 move camera folder and sync 2026-10-02 10:59:07 +00:00
lucas ec48d2ef2d remove custom image from diun container 2026-10-01 16:20:14 +02:00
lucas 0e45389c60 docker compose update mediahost 2026-10-01 14:07:55 +00:00
lucas 72fe77f040 backup docker script
docker compose backup serverhost
2026-10-01 14:07:31 +00:00
lucas 3c69b2c915 fix permissions
chmod
2026-10-01 14:07:30 +00:00
lucas 342d0ddbf5 add custom qbittorrent upgrade to upgrade script
asd
2026-10-01 14:07:30 +00:00
lucas 4ccf2ac412 move and adapt rename camera and memes scripts 2026-10-01 14:07:30 +00:00
lucas 84d5223a95 add custom qbittorrent upgrade to upgrade script 2026-10-01 14:07:29 +00:00
lucas d29489f498 add scp command check to torrent_complete script 2026-10-01 14:07:29 +00:00
lucas 7bcddd9073 example commands for cscli firewall 2026-10-01 14:07:28 +00:00
lucas 286e5fa950 move convert heic script 2026-10-01 14:07:27 +00:00
Lucas Faber d1887382f2 move wol script 2026-10-01 14:07:25 +00:00
Lucas Faber 58baa5f011 revise extract episode from dvd script 2026-10-01 14:07:24 +00:00
lucas 47cfca3c13 set vim as lazygit editor 2026-10-01 14:07:24 +00:00
lucas 11f53f9548 add comment for localhost syncNC 2026-10-01 14:07:23 +00:00
Lucas Faber 6a2fab5e9e sonar translation script copied from chatgpt 2026-10-01 14:07:22 +00:00
Lucas Faber fb7e0e8cff vpn configs
add private keys from netrc to files
2026-10-01 14:07:22 +00:00
lucas 3c7272abe5 add wireguard vpn service and randomize script 2026-10-01 14:07:21 +00:00
lucas fd77d08ef0 add Syncfolder to syncNC 2026-10-01 14:07:21 +00:00
lucas d992905301 move upgrade script
asd
2026-10-01 14:07:20 +00:00
Lucas Faber 0d059bc1c0 first version of old dvd extraction script 2026-10-01 14:07:20 +00:00
Lucas Faber 0c0920d3df add vpn config for surfshark 2026-10-01 14:07:19 +00:00
lucas 73cb3802fb add rename folders with dost function to bash aliases
ads
2026-10-01 14:07:19 +00:00
Lucas Faber f8f0fd6fad add find multiple video files function to bash aliases 2026-10-01 14:07:18 +00:00
lucas b92bdbea7d torrent complete qbittorrent inside docker 2026-10-01 14:07:18 +00:00
lucas 366c26f581 torrent_mqtt remove logging and run inside docker 2026-10-01 14:07:17 +00:00
lucas a1aa5984e1 feat: torrent complete script to copy files to mediahost
gets name, path and category from qbittorrent
sends mqtt command
copies files to mediahost
2026-10-01 14:07:17 +00:00
lucas db90b1ae36 add nfs and qbittorrent to iptables 2026-09-25 14:51:49 +00:00
Lucas Faber 0d8ae83626 refactor(nextcloud): improve version and trash cleanup script
refactor(nextcloud): improve version and trash cleanup script*
- add strict bash error handling
- use trap to always restart apache- fix unsafe array expansion
- improve file deletion handling
- suppoot filenames containing spaces
- replace service with systemctl
- simpmify trashbin cleanup logic
- improve user prompts and output
- add proper root privilege verification
- reduce code duplication
2026-09-25 14:51:49 +00:00
Lucas Faber 3168e3928b feat(sync): refactor Nextcloud sync script with retries and argument handling
feat(sync): refactor Nextcloud sync script with retries and argument
handling

- Add centralized sync_directory() function
- Add retry logic with configurable attempts and delay
- Improve argument parsing and help output
- Add validation for selected sync targets
- Add support for sync marker removal (-del)
- Add logging abstraction with silent/live output modes
- Add dependency and credential file checks
- Prevent concurrent nextcloudcmd executions
- Improve error handling and script structure
- Add version flag support
- Standardize synchronization workflow for all folders

asd
2026-09-25 14:51:49 +00:00
Lucas Faber e5629b73ae refactor(nextcloud): improve OCC maintenance script
refactor(nextcloud): improve OCC maintenance script

- add strict bash error handling
- fix log file initialization for multimedia scan
- fix incorrect completion log entries
- add automatic apache restart via trap
- improve variable naming and quoting
- add maintenance script documentation
- improve timestamp format
- separate user-specific and summary logs
- preserve multiline OCC output
- remove obsolete email functionality
- improve overall readability and reliability

asd
2026-09-25 12:15:40 +02:00
Lucas Faber 653a367d92 remove deprecated scripts 2026-09-25 12:10:02 +02:00
Lucas Faber fcb901e64d move torrent scripts and revise torrentcomplete 2026-09-25 11:57:47 +02:00
Lucas Faber 8d9a32879b add mediahost to bash_aliases 2026-09-25 11:48:21 +02:00
Lucas Faber 6969dae72b lazygit config 2026-09-23 18:27:11 +02:00
Lucas Faber a844fa9609 vpn service aund bashrc 2026-09-23 16:22:04 +00:00
Lucas Vivohost d34cd72f7a vimrc example 2026-09-19 17:01:59 +02:00
Lucas Vivohost 93a0c3ca79 servertunnel to someoneslinux 2026-09-17 12:00:37 +02:00
Lucas Vivohost b1f316667e lockscreen service: add comment and increased sleep 2026-09-14 20:15:03 +02:00
Lucas Vivohost d5cdead474 move services to service dir 2026-09-14 18:47:17 +02:00
lucas 34dfb4da47 loginctrl service to lock screen after login 2026-09-14 18:34:13 +02:00
Lucas Vivohost b32247eff4 initial audiobook python script
intendet to relieve all metadata scripts
2026-09-14 12:47:09 +02:00
Lucas Vivohost da2981a5be refactor: improve torrent transfer workflow
refactor: improve torrent transfer workflow

- replace unsafe word splitting with Bash arrays
- improve command-line argument parsing
- centralize remote and local configuration
- replace remote ls parsing with find
- simplify confirmation handling
- improve SSH and SCP error handling
- safely quote local paths and filenames
- separate copy, rename, and delete operations
- add comprehensive usage documentation
2026-09-14 12:46:09 +02:00
Lucas Vivohost 081210d3ae refactor: optimize terminal progress bar library
refactor: optimize terminal progress bar library

- simplify terminal cursor and scroll-region handling
- replace echo/let with printf and Bash arithmetic
- localize function variables
- centralize progress-bar rendering
- validate and clamp progress percentages
- handle narrow terminal widths safely
- improve Ctrl-C cleanup and exit status
- retain existing public API
- expand inline documentation
2026-09-14 12:44:39 +02:00
Lucas Vivohost 778490f090 feat: add psmouse module reload utility
Add utility to reload psmouse kernel module

- Add psmouse reload script
- Handle unloaded psmouse module
- Add dependency and error checks
- Add usage and troubleshooting documentation
- Provide status messages during reload
2026-09-14 12:44:39 +02:00
Lucas Vivohost daf0457175 refactor: improve SSH local port forwarding script
refactor: improve SSH tunnel helper

- make SSH tunnel configuration explicit
- validate DDNS resolution
- validate SSH private key
- improve quoting and shell safety
- add descriptive status output
- document local and remote forwarding endpoints
- preserve background SSH forwarding behavior
2026-09-14 12:44:39 +02:00
Lucas Vivohost f313e9edfa refactor: optimize PDF image splitting script
refactor: optimize PDF image splitting script

- simplify path and filename handling
- safely quote file paths
- improve temporary directory management
- remove redundant shell pipelines
- simplify image batching
- add error handling and progress output
- document usage and processing steps
2026-09-14 12:44:39 +02:00
Lucas Vivohost a209038263 Generalize PDF-to-JPEG conversion script
feat: accept PDF filename as input argument

- Allow PDF file to be specified on the command line
- Derive output JPEG name from input PDF basename
- Generate files using IMG_<pdf-name>.jpg naming convention
- Retain first-page conversion behavior
- Improve script reusability for multiple PDFs
2026-09-14 12:44:39 +02:00
Lucas Vivohost d0b6444332 feat(admin): improve updatfeat(admin): automate system maintenance and SpotDL environment management
feat(admin): improve update automation and dependency handling

- Add complete script documentation and usage examples
- Create SpotDL virtual environment automatically
- Install pip if missing from the virtual environment
- Install and update spotdl, yt-dlp, and youtube-dl automatically
- Upgrade pip, setuptools, and wheel during maintenance
- Improve logging and status reporting
- Fix update task status message mappings
- Create log directory automatically when missing
- Add Docker image update and restart handling
- Improve shell quoting and error resilience
- Support optional email reporting mode
- Consolidate system maintenance into a single workflow

asd

asd
2026-09-14 12:44:39 +02:00
Lucas Vivohost 09cfddef94 feat(images): rename photos using capture timestamps
feat(images): automate image renaming from EXIF metadata

- Rename images using DateTimeOriginal EXIF timestamps
- Fall back to file modification times when EXIF data is missing
- Support JPG, JPEG, and PNG image formats
- Generate consistent IMG_YYYYMMDD_HHMMSS filenames
- Preserve original file extensions
- Prevent filename collisions using numeric suffixes
- Support processing arbitrary target directories
- Add script documentation, requirements, and usage examples
2026-09-14 12:43:38 +02:00
Lucas Vivohost 804e17dbe6 feat(images): add wallpaper candidate collection utility
feat(images): automate wallpaper preparation and filtering

- Recursively scan image directories for JPG, JPEG, and PNG files
- Normalize filenames by replacing spaces with underscores
- Convert JPG/JPEG images to PNG format
- Calculate image aspect ratios automatically
- Filter images using configurable wallpaper criteria
- Validate minimum image width requirements
- Optionally copy matching wallpapers to a collection folder
- Provide progress reporting during processing
- Add script documentation, requirements, and usage examples
2026-09-14 12:43:38 +02:00
Lucas Vivohost e003cbb11f feat(screenshots): add interactive screenshot capture utility
feat(screenshots): automate screenshot naming and storage

- Detect active Texmaker project directory automatically
- Fall back to manual directory selection when needed
- Prompt user for screenshot filename
- Prevent accidental filename collisions
- Copy screenshot name to clipboard automatically
- Capture screenshots using GNOME Screenshot area selection
- Allow repeated captures until user confirms the result
- Store screenshots directly in project documentation folders
- Add script documentation and usage instructions

ads
2026-09-14 12:43:38 +02:00
Lucas Vivohost 2c2bf379a8 feat(images): add JPG to PNG conversion utility
feat(images): automate JPG/JPEG to PNG conversion

- Recursively scan directories for JPG and JPEG files
- Convert images to PNG using ImageMagick
- Preserve original directory structure
- Remove
2026-09-14 12:43:38 +02:00
Lucas Vivohost aa48304220 feat(images): unify image resize and PNG conversion scripts
feat(images): consolidate resize automation workflow

- Merge automatic and manual resize scripts
- Detect portrait and landscape orientation automatically
- Resize portrait images to x1080
- Resize landscape images to 1920px width
- Support user-defined ImageMagick resize arguments
- Convert JPG/JPEG files to PNG format
- Create output directory automatically
- Normalize filenames by replacing spaces with underscores
- Add date prefix to generated files
- Improve reliability with safer dimension detection
- Add script documentation and usage examples
2026-09-14 12:43:38 +02:00
Lucas Vivohost 3203817e51 feat(audio): embedd album artwork into MP3 files
Add script for attaching cover art to MP3 files using ffmpeg
or lame.

- Support batch processing of all MP3 files in a directory
- Support updating individual MP3 files
- Embed front-cover artwork from a supplied image
- Preserve existing audio streams when using ffmpeg
- Write ID3v2.3-compatible cover metadata
- Replace original files with updated tagged versions
- Add usage examples and script documentation
- Simplify album artwork management for music collections

ads
2026-09-14 12:43:38 +02:00
Lucas Vivohost 8d98c134d0 feat(gnome): create custom keyboard shortcuts
Add script to automate creation of GNOME custom keyboard shortcuts.

- Detect existing custom keybindings
- Find the next available custom shortcut slot
- Register new custom-keybinding entries automatically
- Configure shortcut name, command, and key combination
- Preserve existing GNOME keyboard shortcuts
- Add usage documentation and examples
- Include helper notes for discovering key names with xev
- Simplify scripting and workstation setup automation
2026-09-14 12:43:38 +02:00
Lucas Vivohost fbcbf008ea feat(wallpapers): automate element spectrum download and panorama generation
Add script for downloading and processing element emission spectra
from Wikimedia Commons.

- Read element definitions from get_images.txt
- Retrieve original spectrum images from Wikimedia Commons
- Download element spectrum PNG files automatically
- Generate standardized filenames using atomic metadata
- Resize images to 3840px width
- Crop images to 3840x540 panorama format
- Export processed files to wallpaper collection directory
- Report missing spectrum images
- Automate creation of element-themed desktop wallpapers
2026-09-14 12:43:38 +02:00
Lucas Vivohost 16c69ac7df feat(images): consolidate panorama detection
Combine panorama detection utilities into a single script.

- Merge aspect-ratio based panorama detection with dimension-based
checks
- Detect images with aspect ratio < 0.5 or > 1.8
- Detect images where width exceeds three times the height
- Ignore images already tagged as Panorama
- Scan directories recursively for JPG, JPEG, and PNG files
- Add timestamped logging of detected panorama candidates
- Optionally copy detected panoramas to a review directory
- Improve filename handling using find -print0
- Use identify -format for reliable dimension extraction
- Add dependency validation and robust error handling
- Reduce repeated ImageMagick parsing and simplify maintenance
2026-09-14 12:43:38 +02:00
Lucas Vivohost c1ebc80235 revisit crop image script
feat(images): improve JPG crop script reliability and safety

- Add detailed script header, description, and usage instructions
- Process only JPG/JPEG files
- Handle filenames containing spaces safely
- Verify ImageMagick dependencies before execution
- Use identify -format for reliable dimension detection
- Match images with width exactly 3546px
- Skip previously processed *_c.jpg and *_u.jpg files
- Add error handling for invalid or corrupt images
- Enable strict Bash mode (set -euo pipefail)
- Improve logging and processing feedback

move file

crop images
2026-09-14 12:43:38 +02:00
Lucas Vivohost f84d8d137b add services and bash alias
wol service

asd
2026-09-14 12:43:38 +02:00
Lucas Serverhost 4b75e4b491 chmod 2026-09-14 12:43:38 +02:00
Lucas Serverhost 793d3f61a3 chown lucas for media rsync 2026-09-14 12:43:38 +02:00
Lucas Serverhost 7c8831501a add audiobook sync to syncNC 2026-09-14 12:43:38 +02:00
Lucas Vivohost 7c2e0db615 metadata scripts
album from folder: create album from foldername
read: read and display author, album, grouping of every folder
remove author before title: remove "name - " when matching author
2026-09-14 12:43:38 +02:00
Lucas Vivohost 408566b6ea move files
move scripts
2026-09-14 12:43:38 +02:00
Lucas Serverhost 5cdbf51c1e add logfile to convert heic 2026-09-14 12:43:38 +02:00
Lucas Serverhost 61c3d2f6c2 youtube add cookies and yt2mp3 scrript 2026-09-14 12:43:38 +02:00
Lucas Serverhost 5e3a289f4e update docker and flatpak
upgrade
2026-09-14 12:43:38 +02:00
Lucas Serverhost cc1f9653de add syncNC cmd to rename_camera_folder 2026-09-14 12:43:38 +02:00
Lucas Serverhost 507ff7347d nextcloud scan: remove maintenance mode 2026-09-14 12:43:38 +02:00
Lucas Serverhost 709d5d2b41 metadata scripts
rank from list: remove turtledemo and add grouping name as argument
metadata scan: scan grouping of all files
metadata search replace: add working dir as argument
2026-09-14 12:43:38 +02:00
Lucas Serverhost f27c172a73 convert heic functional script w docker
add docker for new magic version on ubuntu 24
2026-09-14 12:43:38 +02:00
Lucas Serverhost 14c7e69172 metadata scripts add mult pattern and tag v2.4 2026-09-14 12:43:38 +02:00
Lucas Vivohost f5e6920e92 heic script convert mov 2 mo4 2026-09-14 12:43:38 +02:00
Lucas Serverhost 566f76459f interpolate generateAlina py 2026-09-14 12:43:38 +02:00
Lucas Serverhost fe68810e0f metadata script
metadata remove genre: script to remove keywords ie audiobook from genre
rank from list: change pattern and separate author and title, rank is
str now
2026-09-14 12:43:38 +02:00
Lucas Vivohost 0dca4be1f3 fix ./space in rename episodes 2026-09-14 12:43:38 +02:00
Lucas Vivohost c310e57035 convert image script 2026-09-14 12:43:38 +02:00
Lucas Vivohost 05cb3b1b9f audiobook scripts 2026-09-14 12:43:38 +02:00
Lucas Vivohost 83e6d7ee22 metadata scripts 2026-09-14 12:43:38 +02:00
Lucas Vivohost 2044545136 rename epiodes pattern 2026-09-14 12:43:38 +02:00
Lucas Serverhost 973108ab41 change mode 750
delete files
2026-09-14 12:43:38 +02:00
Lucas Vivohost b37ee0bd27 metadata scripts 2026-09-14 09:20:47 +02:00
Lucas Vivohost 57d5cf3d34 silent and metadata for ffmpeg 2026-09-14 09:20:47 +02:00
Lucas Vivohost dbd6dc9e54 torrent scripts 2026-09-14 09:20:47 +02:00
Lucas Vivohost cff21a4dab metadata scripts 2026-09-14 09:20:47 +02:00
Lucas Faber 74a3cfe8ed alina chart 2026-09-14 09:20:47 +02:00
Lucas Faber 3e258c6afe sync & torrent scripts 2026-09-14 09:20:47 +02:00
Lucas Faber 884ae1f841 rtorrent & jellyfin 2026-09-14 09:20:47 +02:00
Lucas Faber 7a4e422bd5 add rename audiobook chapters to ffmpeg handling 2026-09-14 09:20:47 +02:00
Lucas Faber d87d87b5a3 notify users script 2026-09-14 09:20:47 +02:00
Lucas Faber 8379439eed fix directory rename error for ffmpeg handling 2026-09-14 09:20:47 +02:00
Lucas Faber 346aa2e8b6 add csv to Alina Chart 2026-09-14 09:20:47 +02:00
Lucas Faber 4ba8088373 change ffmpeg filter audio to add track
fix
2026-09-14 09:20:47 +02:00
Lucas Faber 0d5f084cd2 add extract-audio and loop through dir for all commands 2026-09-14 09:20:47 +02:00
Lucas Faber e3011ec767 fix base directory name after rename_directories() 2026-09-14 09:20:47 +02:00
Lucas Faber 286ea0252c calculate Datum from Mont/Week 2026-09-14 09:20:47 +02:00
Lucas Faber dff0c12c77 add run_cmd for silent nextcloudcmd
fix syncNC script logging

fix
2026-09-14 09:20:47 +02:00
Lucas Faber 44f30718ac pyproject toml 2026-09-14 09:20:47 +02:00
Lucas Faber 84bf2277de reverseMove script 2026-09-14 09:20:47 +02:00
Lucas Faber ece9132ca1 fix: directory rename and add pattern 2026-09-14 09:20:47 +02:00
Lucas Faber a56a4dfbf6 add interactive menu to ffmpeg script 2026-09-14 09:20:47 +02:00
Lucas Faber c6aa1ac6fb fix: add pathlib and PYTHONPATH for rename camera script 2026-09-14 09:20:47 +02:00
Lucas Faber 6e9e150253 add psgrep instead of temp file
add exclude for git dir

fix
2026-09-14 09:20:47 +02:00
Lucas Faber 8b7f724b2a fix upper case serverlibary 2026-09-14 09:20:47 +02:00
Lucas Faber 46b690deb0 gitignore
fix
2026-09-14 09:20:47 +02:00
Lucas Faber 3e6cf27505 python 2026-09-14 09:20:47 +02:00
Lucas Faber b3c06c0a80 quiethost 2026-09-14 09:20:47 +02:00
Lucas Faber 9c379af76b network 2026-09-14 09:20:47 +02:00
Lucas Faber be436085b3 raspberry 2026-09-14 09:20:47 +02:00
Lucas Faber 84dfdf5210 serverhost
fix
2026-09-14 09:20:47 +02:00
Lucas Faber 24736d0577 vivohost 2026-09-14 09:20:47 +02:00
Lucas Faber 7db38ae9f4 bash scripts 2026-09-14 09:20:47 +02:00
Lucas Faber 7ed4fc67d5 project files 2026-09-14 09:20:47 +02:00
Lucas Faber 9b5727ca45 serverLibary 2026-09-14 09:20:47 +02:00
Lucas Serverhost 830358abbb remove swp files 2026-09-14 07:41:37 +02:00
271 changed files with 21784 additions and 4 deletions

No files matched your search

Executable
+22
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
3.12
Executable
View File
Whitespace-only changes.
+200
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,440 @@
#!/usr/bin/env python3
import subprocess
import tempfile
import argparse
import logging
import re
from pathlib import Path
from typing import Any
from collections import defaultdict
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
RESET = "\033[0m"
logger = logging.getLogger(__name__)
audio_extensions = {
"*.m4b",
"*.m4a",
"*.mp3",
"*.flac",
"*.ogg",
"*.opus"
}
class Audiobook():
def __init__(self, bookPath: Path) -> None:
self.bookPath = bookPath
self.files = [file for pattern in audio_extensions for file in
bookPath.glob(pattern)]
self.firstFile = self.get_first_file()
self.size = sum(file.stat().st_size for pattern in audio_extensions for file in
bookPath.glob(pattern))
self.fileConunt = len(self.files)
self.singleFile = self.fileConunt > 1
# Metadata
self.loadMetadata()
logger.info(f"Found {self.bookPath.name}, {self.fileConunt} files")
def __repr__(self):
return (
f"Audiobook("
f"name={self.name!r}, "
f"artist={self.artist!r}, "
f"composer={self.composer!r}, "
f"path={str(self.bookPath)!r}),"
f"size={self.size!r}, "
f"fileConunt={self.fileConunt!r}, "
)
def get_first_file(self):
return self.files[0]
def loadMetadata(self):
fields = ["artist", "album", "composer", "comment", "genre"]
metadata = self.get_metadata(self.get_first_file(), fields )
self.artist = metadata["artist"]
self.name = metadata["album"]
self.composer = metadata["composer"]
self.comment = metadata["comment"]
self.genre = metadata["genre"]
def get_metadata(self, file, fields):
metadata=dict.fromkeys(fields, "")
for field in fields:
cmd= (["kid3-cli", "-c", f"get {field}", str(file)])
try:
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
check=True,
)
metadata[field] = result.stdout.strip()
except (OSError, subprocess.SubprocessError) as error:
print(f"Error getting metadata for {file}: {error}")
return metadata
return metadata
def write_metadata(self, file, fields, values):
metadata=dict.fromkeys(fields, "")
for field,value in fields,values:
cmd= (["kid3-cli", "-c" , "tag 2","-c", f"set {field} \"{value}\"", "-c" "save", str(file)])
try:
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
check=True,
)
except (OSError, subprocess.SubprocessError) as error:
print(f"Error writing metadata for {file}: {error}")
return
return
def fixComposer(self):
if not self.composer:
m = re.search(
r"(?:read\s+by|spoken\s+by|narrated\s+by|gesprochen\s+von|gelesen\s+von)\s+([^\n.,;]+)",
self.comment,
re.IGNORECASE,
)
if not m:
logger.info(f"No composer found for {self.bookPath}")
return
logger.info(f"[Updated] composer found for {self.bookPath}")
self.composer = m.group(1).strip()
self.comment = (self.comment[:m.start()] + self.comment[m.end():] ).strip(" .,;\n")
self.comment = re.sub(r"\s+"," ",self.comment)
def fixName(self):
if not self.name:
self.name = self.bookPath.name
logger.info(f"[Updated] name found for {self.bookPath}")
def clean_genre(self,genre):
if not self.genre:
return
genre = self.genre.lower()
genre = re.sub(r'(?i)^audio\s*books?$', '', genre)
genre = re.sub(r'(?i)^audiobooks?;?$', '', genre)
genre = re.sub(r'(?i)\b(hörb(uch|ücher)|hoerb(uch|uecher))\b', '', genre)
genre = re.sub(r'(?i)^(hörspiele?|hoerspiele?)$', '', genre)
genre = re.sub(r'(?i)^spoken\s*word$', '', genre)
genre = re.sub(r'(?i)gesprochene\s*inhalte$', '', genre)
genre = re.sub(r'(?i)^speech$', '', genre)
genre = re.sub(r'Fanasie', 'Fantasie', genre)
genre = re.sub(r'Mit Videospiel-?', 'Videospielbezug', genre)
genre = re.sub(r'(?i)nonfiction', 'NonFiction', genre)
genre = re.sub(r'(?i)^speech$', '', genre)
genre = re.sub(r'(?i)^vocal$', '', genre)
genre = re.sub(r'(?i)^retail$', '', genre)
genre = re.sub(r'(?i)^other$', '', genre)
genre = re.sub(r'(?i)^misc$', '', genre)
genre = re.sub(r'(?i)^(sci[- ]?fi|sf)$', 'Science Fiction', genre)
genre = re.sub(r'(?i)^\(?science fiction\)?$', 'Science Fiction', genre)
genre = re.sub(r'Thrillers', 'Thriller', genre)
genre = re.sub(r"(?i)\bchildren'?s audiobooks?\b", "", genre)
genre = re.sub(r'^-\s*|\s*-$', '', genre)
genre = re.sub(r'(?i)Engelske\s*\sLydbånd', '', genre)
genre = re.sub(r'^FIC028000.*$', '', genre)
genre = re.sub(r'^800.*$', '', genre)
genre = re.sub(r'^Fantastic Beasts.*$', '', genre)
genre = re.sub(r'^and thei.*$', '', genre)
genre = re.sub(r'^those you.*$', '', genre)
# Normalize separators
genre = re.sub(r'\s*[,&:/]\s*',';',genre)
genre = re.sub(r'\s*;\s*','; ',genre)
# Trim punctuation / whitespace
genre = re.sub(r'^[\s,;:\-]+','',genre)
genre = re.sub(r'[\s,;\-]+$','',genre)
genre = genre.strip()
self.genre = genre.lstrip()
return
def create_m4b(self, root_path) -> None:
output_file_final = self.bookPath / f"{self.bookPath.name}.m4b"
if output_file_final.exists():
logger.info(f"{RED}[SKIP]{RESET} {output_file_final}")
logger.info("Book already present")
return
logger.info(f"[CREATE] {output_file_final}")
with (tempfile.TemporaryDirectory(dir=root_path.parent) as tmp):
tmpdir = Path(tmp)
output_file_tmp = tmpdir / f"{self.bookPath.name}.m4b"
concat_file = tmpdir / "concat.txt"
metadata_file = tmpdir / "chapters.txt"
current_ms = 0
with concat_file.open("w", encoding="utf-8") as cf, \
metadata_file.open("w", encoding="utf-8") as mf:
mf.write(";FFMETADATA1\n")
for media in self.files:
cf.write(f"file '{ffmpeg_escape(media)}'\n")
duration_ms = self.get_duration_ms(media)
mf.write("[CHAPTER]\n")
mf.write("TIMEBASE=1/1000\n")
mf.write(f"START={current_ms}\n")
mf.write(f"END={current_ms + duration_ms}\n")
mf.write(f"title={media.stem}\n")
current_ms += duration_ms
cmd_backup = ["ffmpeg", "-y", "-f", "concat", "-safe", "0",
"-i", str(concat_file), "-i", str(metadata_file),
# cover from first media
"-i", str(self.firstFile),
# audio from concat
"-map", "0:a",
# cover image from first media
"-map", "2:v?",
# copy artist/album/composer/etc from first media file
"-map_metadata", "2",
# copy chapters from ffmetadata
"-map_chapters", "1",
"-c:a", "copy",
"-c:a", "aac",
"-b:a", "128k",
# copy cover image without re-encoding
"-c:v", "copy", "-disposition:v:0", "attached_pic",
str(output_file_tmp),
]
cmd_default = ["ffmpeg", "-y", "-f", "concat", "-safe", "0",
"-i", str(concat_file), "-i", str(metadata_file),
# cover from first media
"-i", str(self.firstFile),
# audio from concat
"-map", "0:a",
# cover image from first media
"-map", "2:v?",
# copy artist/album/composer/etc from first media file
"-map_metadata", "2",
# copy chapters from ffmetadata
"-map_chapters", "1",
"-c:a", "copy",
# copy cover image without re-encoding
"-c:v", "copy", "-disposition:v:0", "attached_pic",
str(output_file_tmp),
]
result = subprocess.run(
cmd_default,
capture_output=True,
text=True,
)
logger.info(result.stdout)
if result.returncode == 234:
logger.warning(
f"{YELLOW}[WARNING]{RESET} - {self.bookPath.name} - {result.returncode} retrying with aac encoder")
result = subprocess.run(
cmd_backup,
capture_output=True,
text=True,
)
logger.info(result.stdout)
if result.returncode == 0:
output_file_tmp.rename(output_file_final)
for media in self.files:
media.unlink()
self.files = Path(output_file_final)
self.firstFile = Path(output_file_final)
self.fileConunt = 1
self.singleFile = True
self.size = Path(output_file_final).stat().st_size
logger.info(f"{GREEN}[SUCCESS]{RESET} - {self.bookPath.name} - delete media files")
else:
logger.error(
f"{RED}[ERROR]{RESET} - {self.bookPath.name} - Code: {result.returncode}, Stderr: {result.stderr}")
def get_duration_ms(self,media: Path) -> int:
duration = subprocess.check_output(
[
"ffprobe",
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"default=noprint_wrappers=1:nokey=1",
str(media),
],
text=True,
).strip()
return int(float(duration) * 1000)
class AudiobookLibrary:
def __init__(self, libraryPath,logfilename: str="audiobook_library.log"):
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
handlers=[
logging.FileHandler(logfilename),
logging.StreamHandler()
]
)
logger.info('Started')
self.libraryPath = Path(libraryPath)
self.audiobooks = []
self._by_path = {}
self._by_name = defaultdict(list)
self._by_artist = defaultdict(list)
self._by_composer = defaultdict(list)
self.loadAudiobooksFromDir()
self.size = sum( audiobook.size for audiobook in self.audiobooks ) # size in bytes
def add(self, book: Audiobook):
self.audiobooks.append(book)
self._by_path[book.bookPath] = book
self._by_name[book.name].append(book)
if book.artist:
self._by_artist[book.artist].append(book)
if book.composer:
self._by_composer[book.composer].append(book)
def printName(self, books):
if not isinstance(books, list):
books = [books]
for book in books:
if book.name:
logger.info(book.name)
else:
logger.info(f"No name in {book.bookPath.name}")
def get_by_path(self, path):
return self._by_path.get(Path(path))
def get_by_name(self, name):
return self._by_name.get(name, [])
def get_by_artist(self, artist):
return self._by_artist.get(artist, [])
def get_by_composer(self, composer):
return self._by_artist.get(composer, [])
def find_name_contains(self, text):
text = text.lower()
return [
book
for book in self.audiobooks
if text in book.name.lower()
]
def find_artist_contains(self, text):
text = text.lower()
return [
book
for book in self.audiobooks
if book.artist and text in book.artist.lower()
]
def loadAudiobooksFromDir(self) -> tuple[list[Any], int]:
"""
A book directory is any directory containing more than one
audiobook file (.mp3, .m4a, .m4b)
"""
media_files = []
for ext in audio_extensions:
media_files.extend(list(self.libraryPath.rglob( ext)) )
dirs = {file.parent for file in media_files}
for directory in dirs:
self.add(Audiobook(directory))
def loadMetadata(self):
for audiobook in self.audiobooks:
audiobook.loadMetadata()
def fixComposers(self):
for book in self.audiobooks:
book.fixComposer()
def fixNames(self):
for book in self.audiobooks:
book.fixName()
def fixComments(self):
for book in self.audiobooks:
if len(book.comment) < 40:
logger.info(f"Remove short comment: {book.comment} of {book.name}")
book.comment = ""
def ffmpeg_escape(path: Path) -> str:
return str(path.resolve()).replace("'", r"'\''")
def main():
parser = argparse.ArgumentParser(
description="Audiobook Directory Processing"
)
parser.add_argument(
"library",
type=Path,
help="Library path"
)
args = parser.parse_args()
library_dir = args.library
library = AudiobookLibrary(library_dir)
library.fixComposers()
library.fixNames()
library.fixComments()
library.printName(library.get_by_artist("Astrid Lindgren"))
library.printName(library.find_artist_contains("Astrid Lindgren"))
library.printName(library.get_by_name("Pippi Langstrumpf"))
library.printName(library.find_name_contains("Pippi Langstrumpf"))
library.printName(library.get_by_composer("Heike Maklatsch"))
# library.printName(library.get_by_path("/home/lucas/Nextcloud/Audiobooks/Neu/Astrid Lindgren/Junker Nils von Eka (Hoerfest)"))
for audiobook in library.audiobooks:
first_file = audio_files[0]
artist = get_metadata("artist", first_file)
album = get_metadata("album", first_file)
composer = get_metadata("composer", first_file)
comment = get_metadata("comment", first_file)
print("====≠===============================")
print(f"Folder: {library_dir.name}")
print(f"Album: {album}")
print(f"Artist: {artist}")
print(f"Narrator: {composer}")
print(f"Comment: {comment}")
if __name__ == "__main__":
main()
+53
View File
@@ -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
View File
@@ -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
+30
View File
@@ -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
+64
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,172 @@
#!/usr/bin/python3
import json
import re
import sqlite3
import argparse
from pathlib import Path
def load_genre(conn, unique=False):
"""
Return all genres from the books table.
Args:
conn: sqlite3 connection
unique: if True, return unique genres only
Returns:
list[str]
"""
genres = []
for (genres_json,) in conn.execute("SELECT genres FROM books"):
if not genres_json:
continue
try:
genres.extend(json.loads(genres_json))
except Exception:
pass
if unique:
return sorted(set(genres))
return genres
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"-p", "--print-only",
action="store_true",
help="Print data only",
)
parser.add_argument(
"--print-all",
action="store_true",
help="Print all data only",
)
parser.add_argument(
"database",
type=Path,
help="SQL Database Path",
)
args = parser.parse_args()
database = args.database.resolve()
if not database.is_file():
parser.error(f"Database is not a file: {database}")
db = sqlite3.connect(database)
cur = db.cursor()
list_genres = load_genre(cur,True)
if args.print_all:
for genre in list_genres:
print(genre)
return
rows = cur.execute(
"SELECT id, genres FROM books"
).fetchall()
for book_id, genres_json in rows:
if not genres_json:
continue
genres = json.loads(genres_json)
genres_new = []
new_genres = []
print("=================================")
print(f"Before sed: {genres}")
for genre in genres:
if "Drachen" in genre:
pass
if "Fantastic" in genre:
pass
genre = re.sub(r'(?i)^audio\s*books?$', '', genre)
genre = re.sub(r'(?i)^audiobooks?$', '', genre)
genre = re.sub(r'(?i)\b(hörb(uch|ücher)|hoerb(uch|uecher))\b', '', genre)
genre = re.sub(r'(?i)^(hörspiele?|hoerspiele?)$', '', genre)
genre = re.sub(r'(?i)^spoken\s*word$', '', genre)
genre = re.sub(r'(?i)gesprochene\s*inhalte$', '', genre)
genre = re.sub(r'(?i)^speech$', '', genre)
genre = re.sub(r'Fanasie', 'Fantasie', genre)
genre = re.sub(r'Mit Videospiel', 'Videospielbezug', genre)
genre = re.sub(r'(?i)nonfiction', 'NonFiction', genre)
genre = re.sub(r'(?i)^speech$', '', genre)
genre = re.sub(r'(?i)^vocal$', '', genre)
genre = re.sub(r'(?i)^retail$', '', genre)
genre = re.sub(r'(?i)^other$', '', genre)
genre = re.sub(r'(?i)^misc$', '', genre)
genre = re.sub(r'(?i)^(sci[- ]?fi|sf)$', 'Science Fiction', genre)
genre = re.sub(r'(?i)^\(?science fiction\)?$', 'Science Fiction', genre)
genre = re.sub(r'Thrillers', 'Thriller', genre)
genre = re.sub(r"(?i)\bchildren'?s audiobooks?\b", "",genre)
genre = re.sub(r'^-\s*|\s*-$', '', genre)
genre = re.sub(r'(?i)Engelske\s*\sLydbånd','',genre)
genre = re.sub(r'^FIC028000.*$', '', genre)
genre = re.sub(r'^800.*$', '', genre)
genre = re.sub(r'^Fantastic Beasts.*$', '', genre)
genre = re.sub(r'^and thei.*$', '', genre)
genre = re.sub(r'^those you.*$', '', genre)
genre = genre.strip()
genre = genre.lstrip()
if "," in genre:
genre = genre.split(",")
elif " - " in genre:
genre = genre.split(" - ")
elif "Historical Adventure Thriller Mystery" in genre:
genre = genre.split(" ")
if isinstance(genre, str):
if len(genre) > 50 or (5 > len(genre) > 0):
genre = ""
print(f"Too long or too short: {genre}")
genre = genre.strip()
genre = genre.lstrip()
new_genres.append(genre)
elif isinstance(genre, list):
if len(genres) > 1:
genres = [g for g in genres if g]
new_genres.extend(genre)
new_genres = sorted(set(new_genres))
print(f"After sed: {new_genres}")
list_difference = False
print("Write to Database")
if len(new_genres) < len(genres):
list_difference = True
for old,new in zip(genres,new_genres):
if old != new:
list_difference = True
break
if not list_difference:
print("No difference: Dont Commit to Database")
continue
if args.print_only:
print("Print only: Dont Commit to database")
continue
print("Write to Database")
print("=================================")
cur.execute(
"UPDATE books SET genres=? WHERE id=?",
(json.dumps(new_genres), book_id),
)
print("Database Updated")
db.commit()
if __name__ == "__main__":
main()
@@ -0,0 +1,192 @@
#!/usr/bin/env python3
import argparse
import re
import subprocess
from pathlib import Path
from datetime import datetime
def get_genre(file_path):
try:
result = subprocess.run(
["kid3-cli", "-c", "get genre", str(file_path)],
capture_output=True,
text=True,
check=False,
)
return result.stdout.strip()
except Exception:
return ""
def set_genre(file_path, genre):
subprocess.run(
[
"kid3-cli",
"-c",
"tag 2",
"-c",
f"set genre \"{genre}\"",
"-c",
"save",
str(file_path),
],
check=False,
)
def clean_genre(genre):
genre = re.sub(r'(?i)^audio\s*books?$', '', genre)
genre = re.sub(r'(?i)^audiobooks?;?$', '', genre)
genre = re.sub(r'(?i)\b(hörb(uch|ücher)|hoerb(uch|uecher))\b', '', genre)
genre = re.sub(r'(?i)^(hörspiele?|hoerspiele?)$', '', genre)
genre = re.sub(r'(?i)^spoken\s*word$', '', genre)
genre = re.sub(r'(?i)gesprochene\s*inhalte$', '', genre)
genre = re.sub(r'(?i)^speech$', '', genre)
genre = re.sub(r'Fanasie', 'Fantasie', genre)
genre = re.sub(r'Mit Videospiel-?', 'Videospielbezug', genre)
genre = re.sub(r'(?i)nonfiction', 'NonFiction', genre)
genre = re.sub(r'(?i)^speech$', '', genre)
genre = re.sub(r'(?i)^vocal$', '', genre)
genre = re.sub(r'(?i)^retail$', '', genre)
genre = re.sub(r'(?i)^other$', '', genre)
genre = re.sub(r'(?i)^misc$', '', genre)
genre = re.sub(r'(?i)^(sci[- ]?fi|sf)$', 'Science Fiction', genre)
genre = re.sub(r'(?i)^\(?science fiction\)?$', 'Science Fiction', genre)
genre = re.sub(r'Thrillers', 'Thriller', genre)
genre = re.sub(r"(?i)\bchildren'?s audiobooks?\b", "", genre)
genre = re.sub(r'^-\s*|\s*-$', '', genre)
genre = re.sub(r'(?i)Engelske\s*\sLydbånd', '', genre)
genre = re.sub(r'^FIC028000.*$', '', genre)
genre = re.sub(r'^800.*$', '', genre)
genre = re.sub(r'^Fantastic Beasts.*$', '', genre)
genre = re.sub(r'^and thei.*$', '', genre)
genre = re.sub(r'^those you.*$', '', genre)
# Normalize separators
genre = re.sub(
r'\s*[,&:/]\s*',
';',
genre
)
genre = re.sub(
r'\s*;\s*',
'; ',
genre
)
# Trim punctuation / whitespace
genre = re.sub(
r'^[\s,;:\-]+',
'',
genre
)
genre = re.sub(
r'[\s,;\-]+$',
'',
genre
)
genre = genre.strip()
genre = genre.lstrip()
return genre
def find_audio_files(library):
extensions = {".mp3", ".m4a", ".m4b"}
return [
p for p in Path(library).rglob("*")
if p.is_file() and p.suffix.lower() in extensions
]
def main():
parser = argparse.ArgumentParser(
description="Audiobook Genre Cleanup"
)
parser.add_argument(
"library",
help="Library path"
)
parser.add_argument(
"--delete",
action="store_true",
help="Update tags instead of reporting only"
)
parser.add_argument(
"--report",
default="/home/lucas/audiobook_genre_cleanup.txt",
help="Report file"
)
args = parser.parse_args()
files = find_audio_files(args.library)
total = len(files)
processed = 0
matches = 0
modified = 0
with open(args.report, "w", encoding="utf-8") as report:
report.write("Audiobook Genre Cleanup Report\n")
report.write(f"Generated: {datetime.now()}\n")
report.write(f"Library: {args.library}\n")
report.write(f"Total files: {total}\n\n")
for file in files:
processed += 1
if processed % 100 == 0:
print(f"Progress: {processed}/{total} files processed")
genre = get_genre(file)
if not genre:
continue
cleaned = clean_genre(genre)
if genre != cleaned:
matches += 1
status = "MATCH"
if args.delete:
set_genre(file, cleaned)
modified += 1
status = "UPDATED"
line = (
f'[{processed}/{total}] [{status}] '
f'{file} | OLD="{genre}" | NEW="{cleaned}"'
)
print(line)
with open(args.report, "a", encoding="utf-8") as report:
report.write(line + "\n")
with open(args.report, "a", encoding="utf-8") as report:
report.write("\n========== SUMMARY ==========\n")
report.write(f"Files scanned : {total}\n")
report.write(f"Matches found : {matches}\n")
report.write(f"Files updated : {modified}\n")
print("\n========== SUMMARY ==========")
print(f"Files scanned : {total}")
print(f"Matches found : {matches}")
print(f"Files updated : {modified}")
print(f"Report written to: {args.report}")
if __name__ == "__main__":
main()
+203
View File
@@ -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
View File
@@ -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"
+20
View File
@@ -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
+104
View File
@@ -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"
+30
View File
@@ -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
View File
@@ -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
View File
@@ -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
+137
View File
@@ -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
+30
View File
@@ -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
View File
@@ -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
+41
View File
@@ -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
Executable
+63
View File
@@ -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
View File
@@ -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
Executable
+30
View File
@@ -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."
}
}
}
+33
View File
@@ -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."
}
}
}
}
+5
View File
@@ -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/
@@ -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
View File
@@ -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
View File
@@ -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'
+32
View File
@@ -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
@@ -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"
+1375
View File
File diff suppressed because it is too large. Load diff
+183
View File
@@ -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
+293
View File
@@ -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)
+8
View File
@@ -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
+40
View File
@@ -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
Executable
+1
View File
@@ -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")" ' \;
+98
View File
@@ -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
+124
View File
@@ -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
View File
@@ -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
+81
View File
@@ -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
View File
@@ -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"
+105
View File
@@ -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."
+141
View File
@@ -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
+97
View File
@@ -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
+54
View File
@@ -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
+115
View File
@@ -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
+98
View File
@@ -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."
+99
View File
@@ -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
View File
@@ -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
View File
@@ -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
+170
View File
@@ -0,0 +1,170 @@
#!/usr/bin/env python3
import re
import sys
from pathlib import Path
from difflib import SequenceMatcher
def normalize(name):
name = Path(name).stem.lower()
# remove brackets
name = re.sub(r"\(.*?\)|\[.*?\]", "", name)
# replace punctuation with spaces
name = re.sub(r"[^a-z0-9]", " ", name)
# collapse whitespace
name = " ".join(name.split())
return name
def similarity(a, b):
words_a = set(normalize(a).split())
words_b = set(normalize(b).split())
if not words_a or not words_b:
return 0.0
# Jaccard similarity
intersection = words_a & words_b
union = words_a | words_b
return len(intersection) / len(union)
def load_playlist(path):
entries = []
with open(path, encoding="utf-8", errors="ignore") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
entries.append(Path(line).name)
return entries
def compare_playlists(orig_file, new_file, threshold=0.85):
orig_files = load_playlist(orig_file)
new_files = load_playlist(new_file)
print(f"\n=== {orig_file.parent} ===")
print(f"Comparing:")
print(f" {orig_file.name}")
print(f" {new_file.name}")
missing = []
for orig in orig_files:
best_match = None
best_score = 0
for candidate in new_files:
score = similarity(orig, candidate)
if score > best_score:
best_score = score
best_match = candidate
if best_score >= threshold:
pass
# print(
# f"MATCH {best_score:.1%} "
# f"{orig} -> {best_match}"
# )
else:
missing.append((orig, best_score, best_match))
if missing:
print("\nMISSING:")
for orig, score, best_match in sorted(
missing,
key=lambda x: x[1],
reverse=True,
):
print(
f" {orig}"
f" (best={score:.1%}"
f"{', candidate=' + best_match if best_match else ''})"
)
return len(missing)
def compare_directory(root, threshold=0.85):
total_missing = 0
playlist_pairs = 0
for orig_file in root.rglob("*_orig.m3u"):
new_file = orig_file.with_name(
orig_file.name.replace("_orig.m3u", ".m3u")
)
if not new_file.exists():
print(
f"\nWARNING: Missing playlist for "
f"{orig_file}"
)
continue
playlist_pairs += 1
total_missing += compare_playlists(
orig_file,
new_file,
threshold,
)
print("\n" + "=" * 80)
print(f"Playlists compared : {playlist_pairs}")
print(f"Total missing : {total_missing}")
def main():
args = sys.argv[1:]
threshold = 0.85
if len(args) == 3 and args[0] == "--threshold":
threshold = float(args[1])
args = [args[2]]
elif len(args) == 4 and args[0] == "--threshold":
threshold = float(args[1])
args = [args[2], args[3]]
if len(args) == 1:
path = Path(args[0])
if not path.is_dir():
print("Single argument must be a directory")
sys.exit(1)
compare_directory(path, threshold)
elif len(args) == 2:
compare_playlists(
Path(args[0]),
Path(args[1]),
threshold,
)
else:
print(
f"Usage:\n"
f" {Path(sys.argv[0]).name} "
f"<playlist_orig.m3u> <playlist.m3u>\n\n"
f" {Path(sys.argv[0]).name} "
f"<directory>\n\n"
f"Optional:\n"
f" --threshold 0.90"
)
sys.exit(1)
if __name__ == "__main__":
main()
+208
View File
@@ -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
+392
View File
@@ -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()
+6
View File
@@ -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)
+17
View File
@@ -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"
+31
View File
@@ -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
+177
View File
@@ -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
+112
View File
@@ -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
+435
View File
@@ -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
+34
View File
@@ -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"
+42
View File
@@ -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
+22
View File
@@ -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
+50
View File
@@ -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
+5
View File
@@ -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
+9
View File
@@ -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
+11
View File
@@ -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
+11
View File
@@ -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
+11
View File
@@ -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