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.
This commit is contained in:
216
analyze_series.py
Executable file
216
analyze_series.py
Executable file
@@ -0,0 +1,216 @@
|
||||
#!/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()
|
||||
|
||||
Reference in New Issue
Block a user