- Create .editorconfig for consistent coding styles. - Add .envrc for direnv integration. - Include .gitignore to exclude environment and build files. - Implement compare_movies.py and analyze_movies.py for movie library comparison and analysis. - Implement compare_series.py and analyze_series.py for TV series library comparison and analysis. - Add configuration example in config.example.txt. - Create README.md with project overview, setup instructions, and usage examples. - Add LICENSE file for MIT License. - Include flake.nix and flake.lock for Nix-based development environment. - Add USAGE.md for quick start guide and common commands.
217 lines
7.8 KiB
Python
Executable File
217 lines
7.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Analyze TV series missing from Plex to identify common patterns.
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from collections import defaultdict
|
|
import re
|
|
|
|
def analyze_missing_series(report_file='series_comparison_report.json'):
|
|
with open(report_file, 'r') as f:
|
|
data = json.load(f)
|
|
|
|
missing = data.get('missing_from_plex', {})
|
|
|
|
if not missing:
|
|
print("No missing TV series found!")
|
|
return
|
|
|
|
print(f"Analyzing {len(missing)} TV series missing from Plex...\n")
|
|
|
|
# Analyze various attributes
|
|
has_special_chars = []
|
|
has_brackets = []
|
|
has_edition_tag = []
|
|
has_imdb_tag = []
|
|
has_tvdb_tag = []
|
|
recently_released = []
|
|
directory_patterns = defaultdict(int)
|
|
episode_counts = []
|
|
|
|
for title, info in missing.items():
|
|
path = Path(info['path'])
|
|
dirname = path.name
|
|
episode_count = info.get('episode_count', 0)
|
|
episode_counts.append(episode_count)
|
|
|
|
# Special characters in directory name
|
|
if re.search(r'[^a-zA-Z0-9\s\-_\.\(\)\[\]\{\}]', dirname):
|
|
has_special_chars.append((title, dirname))
|
|
|
|
# Brackets/braces patterns
|
|
if '{' in dirname or '}' in dirname:
|
|
has_brackets.append((title, dirname))
|
|
|
|
# Edition tags
|
|
if '{edition-' in dirname.lower():
|
|
has_edition_tag.append((title, dirname))
|
|
|
|
# IMDB/TVDB tags
|
|
if '{imdb-' in dirname.lower():
|
|
has_imdb_tag.append((title, dirname))
|
|
if '{tvdb-' in dirname.lower():
|
|
has_tvdb_tag.append((title, dirname))
|
|
|
|
# Year extraction
|
|
year_match = re.search(r'\((\d{4})\)', dirname)
|
|
if year_match:
|
|
year = int(year_match.group(1))
|
|
if year >= 2020:
|
|
recently_released.append((title, year, dirname))
|
|
|
|
# Directory naming patterns
|
|
# Check for patterns like "Show (Year)" or "Show Name - [Quality]"
|
|
if re.search(r'\((\d{4})\)', dirname):
|
|
directory_patterns['has_year'] += 1
|
|
if re.search(r'\{[^}]+\}', dirname):
|
|
directory_patterns['has_curly_braces'] += 1
|
|
if re.search(r'\[[^\]]+\]', dirname):
|
|
directory_patterns['has_square_brackets'] += 1
|
|
|
|
# Print analysis
|
|
print("="*80)
|
|
print("ANALYSIS RESULTS")
|
|
print("="*80)
|
|
|
|
print(f"\n📊 EPISODE COUNTS:")
|
|
if episode_counts:
|
|
print(f" Total episodes: {sum(episode_counts)}")
|
|
print(f" Average per series: {sum(episode_counts) / len(episode_counts):.1f}")
|
|
print(f" Min: {min(episode_counts)}, Max: {max(episode_counts)}")
|
|
|
|
# Count series with no episodes
|
|
no_episodes = sum(1 for c in episode_counts if c == 0)
|
|
if no_episodes > 0:
|
|
print(f" ⚠️ Series with 0 episodes: {no_episodes}")
|
|
|
|
print(f"\n🔤 NAMING PATTERNS:")
|
|
for pattern, count in sorted(directory_patterns.items(), key=lambda x: x[1], reverse=True):
|
|
pct = (count / len(missing)) * 100
|
|
print(f" {pattern:30} {count:4} ({pct:.1f}%)")
|
|
|
|
if has_imdb_tag:
|
|
print(f"\n🎬 IMDB TAGS: {len(has_imdb_tag)}")
|
|
print(f" Series with {{imdb-...}} tags")
|
|
|
|
if has_tvdb_tag:
|
|
print(f"\n📺 TVDB TAGS: {len(has_tvdb_tag)}")
|
|
print(f" Series with {{tvdb-...}} tags")
|
|
|
|
if has_edition_tag:
|
|
print(f"\n🏷️ EDITION TAGS: {len(has_edition_tag)}")
|
|
print(f" Series with {{edition-...}} tags")
|
|
|
|
if recently_released:
|
|
print(f"\n📅 RECENT SERIES (2020+): {len(recently_released)}")
|
|
recent_sorted = sorted(recently_released, key=lambda x: x[1], reverse=True)
|
|
for title, year, dirname in recent_sorted[:10]:
|
|
print(f" {year} - {title[:60]}")
|
|
if len(recently_released) > 10:
|
|
print(f" ... and {len(recently_released) - 10} more")
|
|
|
|
if has_special_chars:
|
|
print(f"\n⚠️ SPECIAL CHARACTERS: {len(has_special_chars)}")
|
|
special_chars_found = set()
|
|
for title, dirname in has_special_chars:
|
|
chars = re.findall(r'[^a-zA-Z0-9\s\-_\.\(\)\[\]\{\}]', dirname)
|
|
special_chars_found.update(chars)
|
|
print(f" Characters found: {', '.join(repr(c) for c in sorted(special_chars_found))}")
|
|
print(f" Sample directories:")
|
|
for title, dirname in has_special_chars[:5]:
|
|
print(f" • {dirname[:75]}")
|
|
|
|
# Check if all in Jellyfin
|
|
all_in_jellyfin = all(info['in_jellyfin'] for info in missing.values())
|
|
some_in_jellyfin = sum(1 for info in missing.values() if info['in_jellyfin'])
|
|
|
|
print(f"\n📺 JELLYFIN STATUS:")
|
|
print(f" Series also in Jellyfin: {some_in_jellyfin}/{len(missing)} ({(some_in_jellyfin/len(missing)*100):.1f}%)")
|
|
|
|
if some_in_jellyfin == len(missing):
|
|
print(" ✓ ALL missing series are visible in Jellyfin")
|
|
print(" → This suggests a Plex scanning/indexing issue")
|
|
elif some_in_jellyfin > 0:
|
|
print(f" ⚠ {len(missing) - some_in_jellyfin} series not in Jellyfin either")
|
|
print(" → These might have filesystem/permission issues")
|
|
|
|
# Check for .plexignore hints
|
|
print(f"\n🔍 CHECK FOR .PLEXIGNORE:")
|
|
# Check all unique parent directories
|
|
parent_dirs = set()
|
|
for info in missing.values():
|
|
path = Path(info['path'])
|
|
parent_dirs.add(path.parent)
|
|
|
|
found_plexignore = False
|
|
for parent_dir in parent_dirs:
|
|
plexignore_path = parent_dir / '.plexignore'
|
|
if plexignore_path.exists():
|
|
print(f" ⚠️ Found .plexignore at: {plexignore_path}")
|
|
print(f" → Check if these series are listed in it!")
|
|
found_plexignore = True
|
|
|
|
if not found_plexignore:
|
|
print(f" No .plexignore files found in series directories")
|
|
|
|
print("\n" + "="*80)
|
|
print("RECOMMENDATIONS:")
|
|
print("="*80)
|
|
|
|
recommendations = []
|
|
|
|
# Check for .plexignore first
|
|
if plexignore_path.exists():
|
|
recommendations.append(
|
|
"• .plexignore file detected!\n"
|
|
f" Check: {plexignore_path}\n"
|
|
" These series might be explicitly excluded."
|
|
)
|
|
|
|
if some_in_jellyfin == len(missing):
|
|
recommendations.append(
|
|
"• ALL missing series are visible in Jellyfin\n"
|
|
" Actions:\n"
|
|
" 1. Force a full library refresh in Plex\n"
|
|
" 2. Check Plex's TV Shows library scanner settings\n"
|
|
" 3. Verify series naming follows Plex conventions\n"
|
|
" 4. Check Plex server logs for scanner errors"
|
|
)
|
|
|
|
no_episodes = sum(1 for c in episode_counts if c == 0)
|
|
if no_episodes > 0:
|
|
recommendations.append(
|
|
f"• {no_episodes} series have 0 episodes detected\n"
|
|
" These directories might be empty or improperly structured.\n"
|
|
" Plex requires proper season/episode folder structure."
|
|
)
|
|
|
|
if has_special_chars:
|
|
recommendations.append(
|
|
f"• {len(has_special_chars)} series have special characters in names\n"
|
|
" Some characters might cause issues with Plex scanner."
|
|
)
|
|
|
|
for i, rec in enumerate(recommendations, 1):
|
|
print(f"\n{i}. {rec}")
|
|
|
|
if not recommendations:
|
|
print("\n• Series appear normal. Try forcing a Plex library refresh.")
|
|
|
|
print("\n" + "="*80)
|
|
|
|
# List all missing series
|
|
print("\n📋 COMPLETE LIST OF MISSING SERIES:")
|
|
print("="*80)
|
|
for i, (title, info) in enumerate(sorted(missing.items()), 1):
|
|
jf_status = "✓" if info['in_jellyfin'] else "✗"
|
|
ep_count = info.get('episode_count', 0)
|
|
print(f"{i:3}. [{jf_status}] {title} ({ep_count} episodes)")
|
|
print("="*80)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
analyze_missing_series()
|
|
|