Files
plexfin-compare/analyze_series.py
Danilo Reyes e772af13a7 Add initial project files for Jellyfin-Plex Library Checker
- 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.
2025-12-05 01:57:15 -06:00

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()