195 lines
6.0 KiB
Python
195 lines
6.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
TV Show Directory Analyzer
|
|
Scans a TV show directory structure and generates a CSV report with:
|
|
- Show metadata
|
|
- Season folder validation
|
|
- Directory structure issues
|
|
- Size metrics and ratios
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import csv
|
|
from pathlib import Path
|
|
from collections import defaultdict
|
|
|
|
|
|
def get_folder_size_gb(path):
|
|
"""Calculate total size of a folder in GB"""
|
|
total_size = 0
|
|
try:
|
|
for dirpath, dirnames, filenames in os.walk(path):
|
|
for filename in filenames:
|
|
filepath = os.path.join(dirpath, filename)
|
|
try:
|
|
total_size += os.path.getsize(filepath)
|
|
except (OSError, FileNotFoundError):
|
|
pass
|
|
except (PermissionError, OSError):
|
|
pass
|
|
return total_size / (1024 ** 3)
|
|
|
|
|
|
def count_episodes_in_folder(folder_path):
|
|
"""Count files in a folder (proxy for episode count)"""
|
|
try:
|
|
return len([f for f in os.listdir(folder_path)
|
|
if os.path.isfile(os.path.join(folder_path, f))])
|
|
except (PermissionError, OSError):
|
|
return 0
|
|
|
|
|
|
def is_season_folder(folder_name):
|
|
"""Check if folder name matches Season pattern"""
|
|
name_lower = folder_name.lower().strip()
|
|
# Matches: Season 1, season 1, Season1, S01, etc.
|
|
if name_lower.startswith('season'):
|
|
return True
|
|
if name_lower.startswith('s') and len(name_lower) <= 4:
|
|
try:
|
|
int(name_lower[1:])
|
|
return True
|
|
except ValueError:
|
|
pass
|
|
return False
|
|
|
|
|
|
def is_featurette_folder(folder_name):
|
|
"""Check if folder appears to be featurettes"""
|
|
name_lower = folder_name.lower()
|
|
return 'featurette' in name_lower or 'special' in name_lower or 'bonus' in name_lower
|
|
|
|
|
|
def analyze_show_directory(show_path):
|
|
"""
|
|
Analyze a single show directory
|
|
Returns dict with analysis results
|
|
"""
|
|
show_name = os.path.basename(show_path)
|
|
|
|
try:
|
|
subdirs = [d for d in os.listdir(show_path)
|
|
if os.path.isdir(os.path.join(show_path, d))]
|
|
except (PermissionError, OSError) as e:
|
|
return {
|
|
'show_name': show_name,
|
|
'total_size_gb': 0,
|
|
'season_folders': 0,
|
|
'non_season_folders': [],
|
|
'episode_count': 0,
|
|
'size_per_episode': 0,
|
|
'issues': f'Permission/Access Error: {str(e)}'
|
|
}
|
|
|
|
season_folders = []
|
|
non_season_folders = []
|
|
total_episodes = 0
|
|
|
|
for subdir in subdirs:
|
|
subdir_path = os.path.join(show_path, subdir)
|
|
|
|
if is_season_folder(subdir):
|
|
season_folders.append(subdir)
|
|
total_episodes += count_episodes_in_folder(subdir_path)
|
|
else:
|
|
non_season_folders.append(subdir)
|
|
|
|
# Calculate size
|
|
total_size_gb = get_folder_size_gb(show_path)
|
|
|
|
# Calculate size per episode
|
|
size_per_episode = total_size_gb / total_episodes if total_episodes > 0 else 0
|
|
|
|
# Identify issues
|
|
issues = []
|
|
if not season_folders:
|
|
issues.append("NO_SEASON_FOLDERS")
|
|
|
|
non_flagged_issues = [f for f in non_season_folders if not is_featurette_folder(f)]
|
|
if non_flagged_issues:
|
|
issues.append(f"UNEXPECTED_FOLDERS: {', '.join(non_flagged_issues)}")
|
|
|
|
featurette_folders = [f for f in non_season_folders if is_featurette_folder(f)]
|
|
|
|
return {
|
|
'show_name': show_name,
|
|
'total_size_gb': round(total_size_gb, 2),
|
|
'season_folders': len(season_folders),
|
|
'season_folder_names': ', '.join(sorted(season_folders)) if season_folders else 'NONE',
|
|
'non_season_folders': ', '.join(non_season_folders) if non_season_folders else 'None',
|
|
'featurette_folders': ', '.join(featurette_folders) if featurette_folders else 'None',
|
|
'episode_count': total_episodes,
|
|
'size_per_episode': round(size_per_episode, 3),
|
|
'issues': '; '.join(issues) if issues else 'OK'
|
|
}
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) > 1:
|
|
directory = sys.argv[1]
|
|
else:
|
|
directory = input("Enter the path to your TV shows directory: ").strip()
|
|
|
|
directory = os.path.expanduser(directory)
|
|
|
|
if not os.path.isdir(directory):
|
|
print(f"Error: Directory not found: {directory}")
|
|
sys.exit(1)
|
|
|
|
print(f"Scanning directory: {directory}")
|
|
print("This may take a while for large collections...\n")
|
|
|
|
# Get all show directories (first level subdirectories)
|
|
try:
|
|
show_folders = [d for d in os.listdir(directory)
|
|
if os.path.isdir(os.path.join(directory, d))]
|
|
except (PermissionError, OSError) as e:
|
|
print(f"Error accessing directory: {e}")
|
|
sys.exit(1)
|
|
|
|
if not show_folders:
|
|
print("No directories found in the specified path.")
|
|
sys.exit(1)
|
|
|
|
results = []
|
|
for i, show_folder in enumerate(sorted(show_folders), 1):
|
|
show_path = os.path.join(directory, show_folder)
|
|
print(f"[{i}/{len(show_folders)}] Processing: {show_folder}")
|
|
|
|
analysis = analyze_show_directory(show_path)
|
|
results.append(analysis)
|
|
|
|
# Write to CSV
|
|
output_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'show_analysis.csv')
|
|
|
|
fieldnames = [
|
|
'show_name',
|
|
'total_size_gb',
|
|
'season_folders',
|
|
'season_folder_names',
|
|
'non_season_folders',
|
|
'featurette_folders',
|
|
'episode_count',
|
|
'size_per_episode',
|
|
'issues'
|
|
]
|
|
|
|
try:
|
|
with open(output_file, 'w', newline='', encoding='utf-8') as csvfile:
|
|
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
|
writer.writeheader()
|
|
writer.writerows(results)
|
|
|
|
print(f"\n✓ Analysis complete!")
|
|
print(f"✓ Results saved to: {output_file}")
|
|
print(f"✓ Analyzed {len(results)} shows")
|
|
|
|
except (PermissionError, IOError) as e:
|
|
print(f"Error writing CSV file: {e}")
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|