#!/bin/bash # Script to organize MP4 files by studio name with video metadata # Format: Studio - Performer(s) - Title [DD-MM-YYYY] [H:MM] [Quality] [H/V].mp4 # Enable strict error handling set -o pipefail # Catch failures in piped commands # Use environment variable if set (for Docker), otherwise use default path ROOT_DIR="${SOURCE_LIBRARY:-/media/bunker-admin/Internal/plex/xxx/media/local/studios}" GIFS_DIR="${GIFS_PATH:-/media/bunker-admin/Internal/plex/xxx/media/local/gifs}" INBOX_DIR="${INBOX_PATH:-/media/bunker-admin/Internal/plex/xxx/media/local/inbox}" COMPILATIONS_DIR="${COMPILATIONS_PATH:-/media/bunker-admin/Internal/plex/xxx/media/public/compilations}" cd "$ROOT_DIR" || exit 1 # Check if ffprobe is available if ! command -v ffprobe &> /dev/null; then echo "Error: ffprobe is not installed. Please install ffmpeg." exit 1 fi # Function to get video duration in h:mm format get_duration() { local file="$1" local seconds=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file" 2>/dev/null) if [[ -n "$seconds" ]]; then local hours=$((${seconds%.*} / 3600)) local minutes=$(((${seconds%.*} % 3600) / 60)) printf "%d:%02d" "$hours" "$minutes" else echo "0:00" fi } # Function to detect orientation get_orientation() { local file="$1" local width=$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of default=noprint_wrappers=1:nokey=1 "$file" 2>/dev/null) local height=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=noprint_wrappers=1:nokey=1 "$file" 2>/dev/null) if [[ -n "$width" ]] && [[ -n "$height" ]]; then if [[ $height -gt $width ]]; then echo "V" else echo "H" fi else echo "H" fi } # Function to detect video quality get_quality() { local file="$1" local height=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=noprint_wrappers=1:nokey=1 "$file" 2>/dev/null) if [[ -n "$height" ]]; then if [[ $height -ge 2160 ]]; then echo "4K" elif [[ $height -ge 1440 ]]; then echo "1440p" elif [[ $height -ge 1080 ]]; then echo "1080p" elif [[ $height -ge 720 ]]; then echo "720p" elif [[ $height -ge 480 ]]; then echo "480p" else echo "SD" fi else echo "720p" fi } # Function to check if file is already H.265/HEVC encoded is_hevc() { local file="$1" local codec=$(ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 "$file" 2>/dev/null) [[ "$codec" == "hevc" ]] || [[ "$codec" == "h265" ]] } # Function to check if filename has [E] encoded flag has_encoded_flag() { local filename="$1" [[ "$filename" =~ \[E\] ]] } # Function to extract and format date from filename extract_date() { local filename="$1" local date="" # Try YY.MM.DD format (e.g., 25.11.19) if [[ "$filename" =~ ([0-9]{2})\.([0-9]{2})\.([0-9]{2}) ]]; then date="${BASH_REMATCH[3]}-${BASH_REMATCH[2]}-20${BASH_REMATCH[1]}" # Try YYYY.MM.DD format elif [[ "$filename" =~ (20[0-9]{2})\.([0-9]{2})\.([0-9]{2}) ]]; then date="${BASH_REMATCH[3]}-${BASH_REMATCH[2]}-${BASH_REMATCH[1]}" # Try YYYYMMDD format elif [[ "$filename" =~ (20[0-9]{2})([0-9]{2})([0-9]{2}) ]]; then date="${BASH_REMATCH[3]}-${BASH_REMATCH[2]}-${BASH_REMATCH[1]}" # Try DD-MM-YYYY format elif [[ "$filename" =~ ([0-9]{2})-([0-9]{2})-(20[0-9]{2}) ]]; then date="${BASH_REMATCH[1]}-${BASH_REMATCH[2]}-${BASH_REMATCH[3]}" # Try (DD.MM.YYYY) format elif [[ "$filename" =~ \(([0-9]{2})\.([0-9]{2})\.([0-9]{4})\) ]]; then date="${BASH_REMATCH[1]}-${BASH_REMATCH[2]}-${BASH_REMATCH[3]}" fi echo "$date" } # Function to clean and standardize filename clean_filename() { local name="$1" local studio="$2" # Remove common release group tags and patterns (case insensitive) but preserve quality info name=$(echo "$name" | sed -E 's/\.(XXX|[XC]|MP4|WEB|HD)\..*//gi') name=$(echo "$name" | sed -E 's/ (XXX|WEB|HD).*//gi') # Remove existing metadata tags name=$(echo "$name" | sed -E 's/ \[[0-9]+:[0-9]+\]//g') name=$(echo "$name" | sed -E 's/ \[[HV]\]//g') name=$(echo "$name" | sed -E 's/ \[[0-9]{2}-[0-9]{2}-[0-9]{4}\]//g') name=$(echo "$name" | sed -E 's/ \[(4K|1440p|1080p|720p|480p|SD)\]//g') name=$(echo "$name" | sed -E 's/ \[E\]//g') # Remove " rq" suffix (release quality tag) name=$(echo "$name" | sed -E 's/ rq$//gi') # Remove quality tags from title but preserve them for later name=$(echo "$name" | sed -E 's/\.(4K|2160p|1440p|1080p|720p|480p)//gi') name=$(echo "$name" | sed -E 's/ (4K|2160p|1440p|1080p|720p|480p)//gi') name=$(echo "$name" | sed -E 's/_720p$//gi') # Remove dates from the main title name=$(echo "$name" | sed -E 's/\.?[0-9]{2}\.[0-9]{2}\.[0-9]{2}\.?//g') name=$(echo "$name" | sed -E 's/\.?(20[0-9]{2})\.[0-9]{2}\.[0-9]{2}\.?//g') name=$(echo "$name" | sed -E 's/(20[0-9]{2})[0-9]{6}//g') name=$(echo "$name" | sed -E 's/[0-9]{2}-[0-9]{2}-(20[0-9]{2})//g') name=$(echo "$name" | sed -E 's/\([0-9]{2}\.[0-9]{2}\.[0-9]{4}\)//g') # Special handling for OnlyFans if [[ "$studio" == "OnlyFans" ]]; then # Remove "onlyfans" prefix (case insensitive) name=$(echo "$name" | sed -E 's/^[Oo]nly[Ff]ans\.?//g') name=$(echo "$name" | sed -E 's/^[Oo]nly[Ff]ans //g') # Remove year prefix/suffix like "2025" or " 2025 " name=$(echo "$name" | sed -E 's/^20[0-9]{2}\.?//g') name=$(echo "$name" | sed -E 's/^20[0-9]{2} //g') name=$(echo "$name" | sed -E 's/ 20[0-9]{2} / /g') # Remove "xxx" suffix name=$(echo "$name" | sed -E 's/ xxx$//gi') # Don't extract if already has the pattern - just return it # This prevents adding extra dashes when re-processing fi # Remove studio name prefix (case insensitive) name=$(echo "$name" | sed -E "s/^${studio}\.//gi") name=$(echo "$name" | sed -E "s/^${studio} //gi") # Replace dots and underscores with spaces name=$(echo "$name" | tr '._' ' ') # Remove multiple spaces name=$(echo "$name" | sed -E 's/ +/ /g') # Trim leading/trailing spaces and dashes name=$(echo "$name" | sed -E 's/^[\ -]+| +$//g') # Capitalize first letter of each word for better readability name=$(echo "$name" | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2))}1') echo "$name" } # =========================================== # INBOX PROCESSING - Sort files to correct directories # =========================================== echo "=== Processing Inbox Directory ===" if [[ -d "$INBOX_DIR" ]]; then # Search recursively for mp4 files (handles release group folders) inbox_count=$(find "$INBOX_DIR" -type f -name "*.mp4" 2>/dev/null | wc -l) if [[ $inbox_count -gt 0 ]]; then echo "Found $inbox_count file(s) in inbox" find "$INBOX_DIR" -type f -name "*.mp4" | while read -r file; do filename=$(basename "$file") echo "Processing: $filename" # Determine destination based on filename patterns destination="" # Check if file is inside a digest output folder (Clips - * or Scenes - *) parent_dir=$(basename "$(dirname "$file")") if [[ "$parent_dir" == Clips\ -\ * ]] || [[ "$parent_dir" == Scenes\ -\ * ]]; then destination="$GIFS_DIR/$parent_dir" echo " -> GIFs/$parent_dir (digest output)" # Check for OnlyFans content (treated as a regular studio) elif [[ "$filename" =~ ^[Oo]nly[Ff]ans ]] || [[ "$filename" =~ [Oo]nly[Ff]ans\.? ]]; then destination="$ROOT_DIR/OnlyFans" echo " -> Studios/OnlyFans (detected OnlyFans pattern)" # Check for known GIF/clip patterns (short duration indicator or known creators) # Files with [Xs] duration tag are likely GIFs elif [[ "$filename" =~ \[[0-9]+s\] ]]; then # Extract creator name (first part before any separator) if [[ "$filename" =~ ^([A-Za-z0-9_-]+) ]]; then creator="${BASH_REMATCH[1]}" # Check if creator directory exists in gifs if [[ -d "$GIFS_DIR/$creator" ]]; then destination="$GIFS_DIR/$creator" echo " -> GIFs/$creator (detected short clip for existing creator)" else destination="$GIFS_DIR" echo " -> GIFs (detected short clip)" fi else destination="$GIFS_DIR" echo " -> GIFs (detected short clip)" fi # Check file duration for potential GIF content (under 2 minutes) else duration_sec=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file" 2>/dev/null | cut -d. -f1) if [[ -n "$duration_sec" ]] && [[ "$duration_sec" -lt 120 ]]; then # Short file - likely a GIF/clip # Try to match to existing creator if [[ "$filename" =~ ^([A-Za-z0-9_-]+) ]]; then creator="${BASH_REMATCH[1]}" if [[ -d "$GIFS_DIR/$creator" ]]; then destination="$GIFS_DIR/$creator" echo " -> GIFs/$creator (short file, matched creator)" else destination="$GIFS_DIR" echo " -> GIFs (short file < 2min)" fi else destination="$GIFS_DIR" echo " -> GIFs (short file < 2min)" fi else # Regular video - goes to studios # Try to extract studio from filename if [[ "$filename" =~ ^([A-Za-z0-9]+)[\.\-\ ] ]]; then studio="${BASH_REMATCH[1]}" destination="$ROOT_DIR/$studio" mkdir -p "$destination" echo " -> Studios/$studio" else destination="$ROOT_DIR" echo " -> Studios (root)" fi fi fi # Copy file to destination (non-destructive - keeps original in inbox) if [[ -n "$destination" ]]; then mkdir -p "$destination" cp "$file" "$destination/" echo " Copied to: $destination/" fi done # Note: We intentionally do NOT clean up the inbox directory # Files remain in inbox until manually deleted by user echo "Inbox files copied (originals retained in inbox)" else echo "Inbox is empty" fi else echo "Inbox directory not found, skipping" fi echo "" # Find all MP4 files (including root level) find . -mindepth 1 -type f -name "*.mp4" | while read -r file; do # Skip files in excluded directories if [[ "$file" =~ /scripts/ ]] || [[ "$file" =~ /spankbang/ ]] || [[ "$file" =~ /gifs/ ]]; then continue fi # Skip compilation files (files with " - Compilation [H]" or " - Compilation [V]" in name) filename=$(basename "$file") if [[ "$filename" =~ " - Compilation [HV]].mp4"$ ]]; then continue fi dir=$(dirname "$file") depth=$(echo "$dir" | tr -cd '/' | wc -c) dirname=$(basename "$dir") filename=$(basename "$file") # SKIP if file already has proper [H:MM] [quality] [H/V] format and is in correct directory # This check happens BEFORE any ffprobe calls for efficiency # [E] flag is optional - files may or may not have been re-encoded yet if [[ "$filename" =~ \[[0-9]+:[0-9]+\].*\[(4K|1440p|1080p|720p|480p|SD)\].*(\[E\].*)?(\[[HV]\])\.mp4$ ]]; then # File has all metadata tags - check if it's in correct studio folder if [[ $depth -eq 1 ]]; then # Already in a studio folder with proper tags, skip continue fi # Check if in OnlyFans performer subdirectory parent_dir=$(dirname "$dir") parent_name=$(basename "$parent_dir") if [[ "$parent_name" == "OnlyFans" ]] && [[ $depth -eq 2 ]]; then # In OnlyFans performer subdirectory with proper tags, skip continue fi fi # Check if file needs metadata update needs_update=false needs_rename=false # Check if filename already has metadata tags (excluding [E] which is optional) if [[ ! "$filename" =~ \[[0-9]+:[0-9]+\] ]] || [[ ! "$filename" =~ \[[HV]\] ]] || [[ ! "$filename" =~ \[(4K|1440p|1080p|720p|480p|SD)\] ]]; then needs_rename=true fi # Check if filename needs cleaning (has periods, underscores, or dates in title) # Note: We exclude ".mp4" from period check name_without_ext="${filename%.mp4}" if [[ "$name_without_ext" =~ \. ]] || [[ "$name_without_ext" =~ _ ]] || [[ "$filename" =~ [0-9]{2}\.[0-9]{2}\.[0-9]{2} ]] || [[ "$filename" =~ (20[0-9]{2})[0-9]{6} ]] || [[ "$filename" =~ (^|[^0-9])(20[0-9]{2})([^0-9]) ]] || [[ "$filename" =~ XXX ]] || [[ "$filename" =~ 1080p ]] || [[ "$filename" =~ 2160p ]]; then needs_rename=true fi # Extract studio name studio="" # If file is in root, try to extract studio from filename if [[ "$dir" == "." ]]; then if [[ "$filename" =~ ^([A-Za-z0-9]+)\. ]] || [[ "$filename" =~ ^([A-Za-z0-9]+)\ -\ ]]; then studio="${BASH_REMATCH[1]}" else studio="Unknown" fi needs_update=true # Files in root always need to be moved else # Check if already in a performer subdirectory under OnlyFans parent_dir=$(dirname "$dir") parent_name=$(basename "$parent_dir") # If parent is OnlyFans and current dir is a performer folder, skip if [[ "$parent_name" == "OnlyFans" ]] && [[ $depth -eq 2 ]]; then continue fi # Extract from directory name if [[ "$dirname" =~ ^([A-Za-z0-9]+)\. ]]; then studio="${BASH_REMATCH[1]}" elif [[ "$dirname" =~ ^([A-Za-z0-9]+)\ -\ ]]; then studio="${BASH_REMATCH[1]}" else studio="$dirname" fi # Check if file is in wrong directory if [[ $depth -eq 1 ]] && [[ "$dirname" == "$studio" ]]; then # File is already in correct studio folder, check if it needs renaming if [[ "$needs_rename" == true ]]; then needs_update=true fi else # File needs to be moved needs_update=true fi fi # Process file if it needs update or rename if [[ "$needs_update" == true ]] || [[ "$needs_rename" == true ]]; then echo "Analyzing: $file" duration=$(get_duration "$file") orientation=$(get_orientation "$file") quality=$(get_quality "$file") date=$(extract_date "$filename") # Create studio directory if it doesn't exist if [[ -n "$studio" ]]; then mkdir -p "$ROOT_DIR/$studio" # Clean and standardize filename clean_name=$(clean_filename "${filename%.*}" "$studio") ext="${filename##*.}" # For OnlyFans, ensure proper format: OnlyFans - Name - Description if [[ "$studio" == "OnlyFans" ]]; then # Check if clean_name already has " - Name - Description" pattern if [[ "$clean_name" =~ ^([A-Z][a-z]+([\ ][A-Z][a-z]+){0,3})[\ ]-[\ ](.+)$ ]]; then # Already has the proper format, just add OnlyFans prefix performer="${BASH_REMATCH[1]}" description="${BASH_REMATCH[3]}" new_filename="OnlyFans - ${performer} - ${description}" # Try to extract performer name (typically first 1-3 capitalized words) elif [[ "$clean_name" =~ ^([A-Z][a-z]+([\ ][A-Z][a-z]+){0,2})([\ ].+)$ ]]; then # Has "Name Description" pattern - add separator performer="${BASH_REMATCH[1]}" description="${BASH_REMATCH[3]}" description=$(echo "$description" | sed -E 's/^ +//g') new_filename="OnlyFans - ${performer} - ${description}" else # Can't extract name, just add OnlyFans prefix # Clean up any leading dashes or spaces clean_name=$(echo "$clean_name" | sed -E 's/^[\ -]+//g') if [[ -n "$clean_name" ]]; then new_filename="OnlyFans - ${clean_name}" else new_filename="OnlyFans" fi fi # For other studios with performer names in title elif [[ "$studio" =~ ^(IntimatePOV|LegalPorno|AccidentalGangbang|18Lust|BlackedRaw|PublicBang|OnlyTarts|Wifey)$ ]]; then # Try to intelligently separate performer from title # Look for common title indicator words if [[ "$clean_name" =~ ^([A-Z][a-z]+([\ ][A-Z][a-z]+){0,3})([\ ](Is|A|The|With|For|On|Of|To|In|Her|Your|My|Lets|Pretend|Seducing|Doubling|Naked|Morning|Sub|Likes|Over).+)$ ]]; then performer="${BASH_REMATCH[1]}" title="${BASH_REMATCH[3]}" title=$(echo "$title" | sed -E 's/^ +//g') new_filename="${studio} - ${performer} - ${title}" else # No clear separator, just use studio and clean name # Clean up any leading dashes or spaces clean_name=$(echo "$clean_name" | sed -E 's/^[\ -]+//g') if [[ -n "$clean_name" ]]; then new_filename="${studio} - ${clean_name}" else new_filename="${studio}" fi fi else # For unknown studios, clean up leading dashes/spaces clean_name=$(echo "$clean_name" | sed -E 's/^[\ -]+//g') if [[ -n "$clean_name" ]]; then new_filename="${clean_name}" else new_filename="${studio}" fi fi # Determine if file should have [E] encoded flag # Preserve existing [E] flag or add if video is HEVC encoded encoded_flag="" if has_encoded_flag "$filename"; then encoded_flag=" [E]" elif is_hevc "$file"; then encoded_flag=" [E]" echo " Adding [E] flag: Already HEVC encoded" fi # Add metadata tags # Format: [date] [duration] [quality] [E] [orientation] if [[ -n "$date" ]]; then new_filename="${new_filename} [${date}] [${duration}] [${quality}]${encoded_flag} [${orientation}].${ext}" else new_filename="${new_filename} [${duration}] [${quality}]${encoded_flag} [${orientation}].${ext}" fi # Clean up any multiple dashes that may have been created new_filename=$(echo "$new_filename" | sed -E 's/ - - - / - /g' | sed -E 's/ - - / - /g') # Set target path (all studios use ROOT_DIR) target_path="$ROOT_DIR/$studio/$new_filename" # Only move/rename if target is different from source if [[ "$file" != "./$studio/$new_filename" ]]; then echo "Processing: $file -> $studio/$new_filename" mv "$file" "$target_path" fi fi fi done echo "" echo "Done organizing studio files!" echo "" echo "=== Organizing GIFs folder ===" # Function to get duration in seconds for gifs get_duration_seconds() { local file="$1" local seconds=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file" 2>/dev/null) if [[ -n "$seconds" ]]; then printf "%.0f" "$seconds" else echo "0" fi } if [[ -d "$GIFS_DIR" ]]; then cd "$GIFS_DIR" || exit 1 # Process each creator directory for creator_dir in */; do if [[ -d "$creator_dir" ]]; then creator_name="${creator_dir%/}" echo "Processing: $creator_name" # Create horizontal/vertical subdirs if they don't exist mkdir -p "$creator_name/horizontal" mkdir -p "$creator_name/vertical" # Process ALL mp4 files in creator directory (root and subdirs) find "$creator_name" -type f -name "*.mp4" | while read -r file; do filename=$(basename "$file") file_dir=$(dirname "$file") # SKIP compilation files - they should stay in the creator root directory # Match ANY file with " - Compilation " in the name (whether freshly created or already tagged) if [[ "$filename" == *" - Compilation "* ]]; then # This is a generated compilation file # Move it to creator root if it's in a subdirectory if [[ "$file_dir" != "$creator_name" ]]; then echo " Moving compilation to root: $filename" mv "$file" "$creator_name/$filename" fi continue fi # SKIP if file already has proper [Xs] [quality] [H/V] format # This check happens BEFORE any ffprobe calls for efficiency # [E] flag is optional - files may or may not have been re-encoded yet if [[ "$filename" =~ \[[0-9]+s\].*\[(4K|1440p|1080p|720p|480p|SD)\].*(\[E\].*)?(\[[HV]\])\.mp4$ ]]; then # File is already fully processed # Just check if it's in the correct H/V subdirectory if [[ "$filename" =~ \[H\]\.mp4$ ]] && [[ "$file_dir" == *"/horizontal" ]]; then continue # Already in correct place elif [[ "$filename" =~ \[V\]\.mp4$ ]] && [[ "$file_dir" == *"/vertical" ]]; then continue # Already in correct place fi # Move to correct subdirectory without re-analyzing if [[ "$filename" =~ \[H\]\.mp4$ ]]; then target_dir="$creator_name/horizontal" else target_dir="$creator_name/vertical" fi if [[ "$file" != "$target_dir/$filename" ]]; then echo " Moving: $filename -> $target_dir/" mv "$file" "$target_dir/$filename" fi continue fi # File needs processing - now run ffprobe echo " Analyzing: $filename" orientation=$(get_orientation "$file") quality=$(get_quality "$file") duration_sec=$(get_duration_seconds "$file") # Clean filename - remove existing tags and extension clean_name="${filename%.*}" clean_name=$(echo "$clean_name" | sed -E 's/ \[[0-9]+:[0-9]+\]//g') clean_name=$(echo "$clean_name" | sed -E 's/ \[[0-9]+s\]//g') clean_name=$(echo "$clean_name" | sed -E 's/ \[[HV]\]//g') clean_name=$(echo "$clean_name" | sed -E 's/ \[(4K|1440p|1080p|720p|480p|SD)\]//g') clean_name=$(echo "$clean_name" | sed -E 's/ \[E\]//g') # Determine if file should have [E] encoded flag encoded_flag="" if has_encoded_flag "$filename"; then encoded_flag=" [E]" elif is_hevc "$file"; then encoded_flag=" [E]" echo " Adding [E] flag: Already HEVC encoded" fi # Build new filename with metadata # Format: [Xs] [quality] [E] [orientation] new_filename="${clean_name} [${duration_sec}s] [${quality}]${encoded_flag} [${orientation}].mp4" # Determine target directory based on orientation if [[ "$orientation" == "H" ]]; then target_dir="$creator_name/horizontal" else target_dir="$creator_name/vertical" fi # Only move/rename if needed if [[ "$file" != "$target_dir/$new_filename" ]]; then echo " -> $target_dir/$(basename "$new_filename")" mv "$file" "$target_dir/$new_filename" fi done # Clean up empty subdirectories (like old square folders if emptied) find "$creator_name" -mindepth 1 -type d -empty -delete 2>/dev/null fi done cd "$ROOT_DIR" || exit 1 fi echo "" echo "Cleaning up old directories..." # Remove the now-empty source directories that match typical release naming patterns find . -mindepth 1 -maxdepth 1 -type d \( -name "*XXX*" -o -name "*[XC]" -o -name "*.MP4-*" \) -exec rm -rf {} + 2>/dev/null # Remove any remaining empty directories find . -mindepth 1 -type d -empty -delete 2>/dev/null echo "" echo "Complete!"