3 Commits

Author SHA1 Message Date
Phoenix 487b28e682 Add Plex Sonarr integration script and testing framework
- Implemented a Python script for integrating Plex Media Server with Sonarr to manage TV show seasons based on viewing habits.
- Added features for automated season management, exclusion list support, and detailed logging.
- Created a requirements.txt file to include necessary packages: plexapi, requests, pytest, pytest-mock, and requests-mock.
- Developed unit tests for the API verification function using pytest and requests_mock.
- Included a standalone script to unmonitor excluded shows in Sonarr.
- Added a test script for manual API verification with mock and real API tests.
2025-08-20 23:59:05 +08:00
phoenix 5413d621d4 Merge branch 'phoenix' into mediascheduler 2024-12-26 15:14:52 +08:00
Phoenix 66e40ff077 Add environment configuration and media processing scripts
- Create .env file for environment variables
- Implement mediascheduler.py for managing TV shows in Plex and Sonarr
- Add mediatask.py for processing series and movies with Telegram notifications
2024-12-26 15:02:04 +08:00
16 changed files with 1126 additions and 8 deletions
+1
View File
@@ -0,0 +1 @@
LOG_ENABLED=true
+11
View File
@@ -0,0 +1,11 @@
{
"python.testing.unittestArgs": [
"-v",
"-s",
".",
"-p",
"*_test.py"
],
"python.testing.pytestEnabled": false,
"python.testing.unittestEnabled": true
}
+219
View File
@@ -0,0 +1,219 @@
# Testing Guide for verify_sonarr_api Function
This guide explains how to test the `verify_sonarr_api` function from the plexsonarr.py script.
## Test Files Overview
### 1. test_verify_api.py - Simple Interactive Test
A user-friendly test script that provides:
- ✅ Mocked success tests
- ❌ Mocked failure tests
- 🧪 HTTP error simulation
- 🌐 Real API testing (optional)
**Usage:**
```bash
python test_verify_api.py
```
### 2. test_plexsonarr.py - Comprehensive pytest Suite
Full test suite using pytest framework that covers:
- Successful API verification
- HTTP errors (401, 404, 500)
- Connection errors
- Timeout scenarios
- Invalid JSON responses
**Usage:**
```bash
pytest test_plexsonarr.py -v
```
## Quick Start
### Install Dependencies
```bash
pip install -r requirements.txt
```
### Run Simple Tests
```bash
python test_verify_api.py
```
### Run Comprehensive Tests
```bash
pytest test_plexsonarr.py -v
```
### Run All Tests
```bash
pytest -v
```
## Test Scenarios Covered
### ✅ Success Cases
- Valid API response (200 OK)
- Correct API key and URL
- Successful connection to Sonarr
### ❌ Error Cases
- **401 Unauthorized**: Invalid API key
- **404 Not Found**: Incorrect endpoint or server
- **500 Server Error**: Sonarr internal error
- **Connection Error**: Network issues
- **Timeout**: Slow response from server
### 🔧 Edge Cases
- Invalid JSON response
- Malformed responses
- Network connectivity issues
## Configuration
The tests use the same configuration as your main script:
```python
SONARR_API_KEY = "2537de37fded4874ae83da9cf3c14f34"
SONARR_SERVER_URL = "http://192.168.50.111:8989"
```
## Understanding Test Output
### Successful Test Run
```
🚀 Starting verify_sonarr_api tests...
✅ Mock success test passed!
✅ Mock failure test passed!
✅ Mock HTTP error test passed!
✅ Real API test passed! Your Sonarr API is working.
```
### Failed Real API Test
```
❌ Failed to verify Sonarr API: HTTPSConnectionPool(host='192.168.50.111', port=8989)
💡 Tip: Check your SONARR_SERVER_URL and SONARR_API_KEY configuration
```
## Pytest Output Explanation
```bash
pytest test_plexsonarr.py -v
```
Each test case shows:
- **PASSED**: Test completed successfully
- **FAILED**: Test found an issue
- **Coverage**: Which scenarios were tested
## Troubleshooting
### Common Issues
1. **Import Errors**
```
ModuleNotFoundError: No module named 'pytest'
```
**Solution**: Install dependencies with `pip install -r requirements.txt`
2. **Connection Refused**
```
ConnectionError: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))
```
**Solution**: Check if Sonarr is running and accessible
3. **API Key Invalid**
```
401 Unauthorized
```
**Solution**: Verify your SONARR_API_KEY in the configuration
### Debugging Tips
1. **Check Sonarr Status**
```bash
curl http://192.168.50.111:8989/api/v3/system/status?apikey=YOUR_API_KEY
```
2. **Test Network Connectivity**
```bash
ping 192.168.50.111
telnet 192.168.50.111 8989
```
3. **Verify API Key**
- Open Sonarr web interface
- Go to Settings → General
- Check the API Key in Security section
## Test Development
### Adding New Tests
To add a new test case to `test_plexsonarr.py`:
```python
def test_verify_sonarr_api_new_scenario(self):
"""Test description"""
with requests_mock.Mocker() as m:
expected_url = f"{plexsonarr.SONARR_SERVER_URL}/api/v3/system/status?apikey={plexsonarr.SONARR_API_KEY}"
m.get(expected_url, status_code=YOUR_STATUS_CODE)
# Your test logic here
with pytest.raises(ExpectedException):
plexsonarr.verify_sonarr_api()
```
### Test Best Practices
1. **Mock External Calls**: Always mock HTTP requests in unit tests
2. **Test Edge Cases**: Include error scenarios and unusual responses
3. **Clear Test Names**: Use descriptive test function names
4. **Isolated Tests**: Each test should be independent
5. **Verify Behavior**: Check both success and failure paths
## Continuous Integration
Add to your CI pipeline:
```yaml
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.11
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run tests
run: pytest test_plexsonarr.py -v
```
## Performance Testing
For load testing the API:
```bash
# Install httpie for testing
pip install httpie
# Test API endpoint directly
http GET "http://192.168.50.111:8989/api/v3/system/status?apikey=YOUR_API_KEY"
```
## Summary
The testing setup provides:
- 🔍 **Comprehensive Coverage**: All success/failure scenarios
- 🚀 **Easy Execution**: Simple commands to run tests
- 📊 **Clear Reporting**: Detailed output and error messages
- 🛠️ **Development Ready**: Easy to extend with new test cases
- 🌐 **Real-world Testing**: Option to test against actual Sonarr API
This ensures your `verify_sonarr_api` function works correctly in all scenarios!
Binary file not shown.
+155
View File
@@ -0,0 +1,155 @@
import os
from dotenv import load_dotenv
from plexapi.server import PlexServer
import requests
import logging
# Load environment variables from .env file
load_dotenv()
LOG_ENABLED= False
PLEX_TOKEN = os.getenv("PLEX_TOKEN")
PLEX_SERVER_URL = os.getenv("PLEX_SERVER_URL")
SONARR_API_KEY = os.getenv("SONARR_API_KEY")
SONARR_SERVER_URL = os.getenv("SONARR_SERVER_URL")
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID")
# Set up logging
if LOG_ENABLED:
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
else:
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
#logging.getLogger().setLevel(logging.CRITICAL)
PLEX_TOKEN = "uZn42JMVkQpyb_duFsvT"
PLEX_SERVER_URL = "http://192.168.50.111:32400"
SONARR_API_KEY = "2537de37fded4874ae83da9cf3c14f34"
SONARR_SERVER_URL = "http://192.168.50.111:8989"
plex = PlexServer(PLEX_SERVER_URL, PLEX_TOKEN)
tv_shows = plex.library.section('TV Shows')
# List of TV show titles to exclude
exclude_shows = ["Stargate SG-1", "Space Sheriff Gavan", "Spider-Man and His Amazing Friends","Super Sentai", "Superman & Lois", "UFO Robot Grendizer","Zorro",
"Saved by the Bell: The College Years","Saber Rider and the Star Sheriffs","Prison Break","Power Rangers","The Outer Limits (1995)","MacGyver","Knight Rider",
"Chuck","Breaking Bad","Amazing Stories (1985)","Airwolf","The Adventures of Superboy (1988)"]
def unmonitor_all_excluded_shows():
for show_title in exclude_shows:
show = tv_shows.get(title=show_title)
if LOG_ENABLED:
logging.info(f"Unmonitor Title: {show.title}")
tvdb_id = get_tvdb_id(show)
series_id = get_series_id_from_tvdb(tvdb_id)
seasons = show.seasons()
for season in seasons:
mark_season_unmonitored(series_id, season.index)
logging.info(f"All seasons of '{show_title}' have been marked as unmonitored.")
# Function to check if the last 3 episodes of a season are watched
def last_3_episodes_watched(season):
episodes = sorted(season.episodes(), key=lambda ep: ep.index)
return all(ep.isWatched for ep in episodes[-3:])
# Function to check if the first 3 episodes of a season are watched
def first_3_episodes_watched(season):
episodes = sorted(season.episodes(), key=lambda ep: ep.index)
return all(ep.isWatched for ep in episodes[:3])
# Function to check if Sonarr API is alive and the token is correct
def verify_sonarr_api():
url = f"{SONARR_SERVER_URL}/api/v3/system/status?apikey={SONARR_API_KEY}"
try:
response = requests.get(url)
response.raise_for_status()
if LOG_ENABLED:
logging.info("Sonarr API is alive and the token is correct.")
except requests.exceptions.RequestException as e:
logging.error(f"Failed to verify Sonarr API: {e}")
raise
# Function to get the series ID from the TVDB ID
def get_series_id_from_tvdb(tvdb_id):
url = f"{SONARR_SERVER_URL}/api/v3/series/lookup?term=tvdb:{tvdb_id}&apikey={SONARR_API_KEY}"
response = requests.get(url)
response.raise_for_status()
series = response.json()
if series:
if LOG_ENABLED:
logging.info(f"Series found: {series[0]['title']} (ID: {series[0]['id']})")
return series[0]['id']
else:
raise ValueError(f"No series found for TVDB ID: {tvdb_id}")
# Function to mark a season as unmonitored in Sonarr v4
def mark_season_unmonitored(series_id, season_number):
url = f"{SONARR_SERVER_URL}/api/v3/series/{series_id}?apikey={SONARR_API_KEY}"
response = requests.get(url)
response.raise_for_status()
series = response.json()
for season in series['seasons']:
if season['seasonNumber'] == season_number:
season['monitored'] = False
response = requests.put(url, json=series)
response.raise_for_status()
logging.info(f"Season {season_number} marked as unmonitored for series ID {series_id}.")
def get_tvdb_id(show):
for guid in reversed(show.guids):
if guid.id.startswith("tvdb"):
return guid.id.split("://")[1]
raise ValueError(f"No TVDB ID found for show: {show.title}")
def get_total_seasons(show):
return len(show.seasons())
def verify_total_seasons_from_tvdb(show, series_id):
url = f"{SONARR_SERVER_URL}/api/v3/series/{series_id}?apikey={SONARR_API_KEY}"
response = requests.get(url)
response.raise_for_status()
series_data = response.json()
# logging.info(series_data)
season_count = series_data.get('statistics', {}).get('seasonCount')
if LOG_ENABLED:
logging.info(f"Total seasons for {show.title} from TVDB ({season_count}) instead Available on Plex ({len(show.seasons())})")
# Verify Sonarr API before proceeding
verify_sonarr_api()
# Call this function after processing the main shows
# unmonitor_all_excluded_shows()
# Iterate over all TV shows and apply the deletion rules
for show in tv_shows.all():
try:
if show.title not in exclude_shows:
logging.info(f"Processing TV Show: {show.title}")
# Verify total seasons from TVDB
tvdb_id = get_tvdb_id(show)
series_id = get_series_id_from_tvdb(tvdb_id)
verify_total_seasons_from_tvdb(show, series_id)
seasons = sorted(show.seasons(), key=lambda s: s.index)
if len(seasons) > 1: # Ensure there is a previous season to delete
latest_season = seasons[-1]
if len(latest_season.episodes()) >= 3 and first_3_episodes_watched(latest_season):
logging.info(f"TVDB ID: {tvdb_id}")
for season in seasons[:-1]: # Excluding the latest season
mark_season_unmonitored(series_id, season.index)
logging.info(f" - Marking Season {season.index} as unmonitored and ready to delete")
else:
for episode in latest_season.episodes():
if episode.isWatched:
logging.info(f" - Watched Episode: {episode.title} (Episode Number: {episode.episodeNumber})")
except Exception as e:
logging.error(f"Error processing show '{show.title}': {e}")
#break
# Send a report message with the list of TV series and movies being processed
""" report_message = "TV Series and Movies Being Processed:\n\n"
report_message += "* Number of Shows: {}\n".format(len(tv_shows.all()))
for show in tv_shows.all():
report_message += "- {} (Last Season)\n".format(show.title)
logging.info("Sending Telegram message with the report...")
response = requests.post(
"https://api.telegram.org/bot{}/sendMessage".format(TELEGRAM_BOT_TOKEN),
data={
'chat_id': TELEGRAM_CHAT_ID,
'text': report_message
}
)
if response.status_code == 200:
logging.info("Telegram message sent successfully.")
else:
logging.error("Failed to send Telegram message.") """
+96
View File
@@ -0,0 +1,96 @@
import requests
import datetime
import telegram
# Configuration
SONARR_API_URL = 'http://localhost:8989/api'
SONARR_API_KEY = 'your_sonarr_api_key'
RADARR_API_URL = 'http://localhost:7878/api'
RADARR_API_KEY = 'your_radarr_api_key'
PLEX_API_URL = 'http://localhost:32400'
PLEX_API_KEY = 'your_plex_api_key'
TELEGRAM_BOT_TOKEN = 'your_telegram_bot_token'
TELEGRAM_CHAT_ID = 'your_telegram_chat_id'
EXCEPTION_SERIES = ['Series1', 'Series2'] # Add your exception series here
# Initialize Telegram bot
bot = telegram.Bot(token=TELEGRAM_BOT_TOKEN)
def get_plex_last_watched():
# Implement function to get last watched date from Plex
pass
def disable_series_in_sonarr(series_id):
url = f"{SONARR_API_URL}/series/{series_id}"
headers = {'X-Api-Key': SONARR_API_KEY}
data = {'monitored': False}
response = requests.put(url, json=data, headers=headers)
return response.status_code == 200
def disable_movie_in_radarr(movie_id):
url = f"{RADARR_API_URL}/movie/{movie_id}"
headers = {'X-Api-Key': RADARR_API_KEY}
data = {'monitored': False}
response = requests.put(url, json=data, headers=headers)
return response.status_code == 200
def list_non_hd_movies():
# Implement function to list non-HD movies from Radarr
pass
def send_telegram_message(message):
bot.send_message(chat_id=TELEGRAM_CHAT_ID, text=message, parse_mode=telegram.ParseMode.MARKDOWN)
def process_series():
# Get all series from Sonarr
response = requests.get(f"{SONARR_API_URL}/series", headers={'X-Api-Key': SONARR_API_KEY})
series_list = response.json()
processed_series = []
for series in series_list:
if series['title'] in EXCEPTION_SERIES:
continue
last_watched = get_plex_last_watched(series['title'])
if last_watched and (datetime.datetime.now() - last_watched).days > 90:
if disable_series_in_sonarr(series['id']):
processed_series.append(series['title'])
return processed_series
def process_movies():
# Get all movies from Radarr
response = requests.get(f"{RADARR_API_URL}/movie", headers={'X-Api-Key': RADARR_API_KEY})
movie_list = response.json()
processed_movies = []
for movie in movie_list:
if get_plex_last_watched(movie['title']):
if disable_movie_in_radarr(movie['id']):
processed_movies.append(movie['title'])
return processed_movies
def main():
# Process series and movies
processed_series = process_series()
processed_movies = process_movies()
# List non-HD movies
non_hd_movies = list_non_hd_movies()
# Send Telegram message
message = "*Processed TV Series and Movies*\n\n"
message += "*TV Series:*\n"
for i, series in enumerate(processed_series, 1):
message += f"{i}. {series}\n"
message += "\n*Movies:*\n"
for i, movie in enumerate(processed_movies, 1):
message += f"{i}. {movie}\n"
message += "\n*Non-HD Movies:*\n"
for movie in non_hd_movies:
message += f"- {movie}\n"
send_telegram_message(message)
if __name__ == "__main__":
main()
+160
View File
@@ -0,0 +1,160 @@
# Plex Sonarr Integration Script
This Python script integrates Plex Media Server with Sonarr to automatically manage TV show seasons based on viewing habits. It can unmonitor seasons in Sonarr and optionally delete them from Plex when certain conditions are met.
## Features
- **Automated Season Management**: Automatically marks previous seasons as unmonitored in Sonarr when the latest season's last 3 episodes are watched
- **Exclude List Support**: Maintains a list of TV shows that should be excluded from automated processing
- **TVDB Integration**: Uses TVDB IDs to match shows between Plex and Sonarr
- **Safe Operation**: Includes verification of Sonarr API connectivity before processing
- **Detailed Logging**: Provides comprehensive output for monitoring and debugging
## Prerequisites
### Required Python Packages
```bash
pip install plexapi requests
```
### Required Services
- **Plex Media Server**: Running and accessible
- **Sonarr v3**: Running and accessible with API access enabled
## Configuration
Before running the script, you need to update the following configuration variables at the top of the script:
```python
PLEX_TOKEN = "your_plex_token_here"
PLEX_SERVER_URL = "http://your_plex_server:32400"
SONARR_API_KEY = "your_sonarr_api_key_here"
SONARR_SERVER_URL = "http://your_sonarr_server:8989"
```
### Getting Your Plex Token
1. Sign in to your Plex account
2. Visit: https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/
3. Follow the instructions to find your X-Plex-Token
### Getting Your Sonarr API Key
1. Open Sonarr web interface
2. Go to Settings → General
3. Copy the API Key from the Security section
## How It Works
### Main Processing Logic
1. **API Verification**: Verifies Sonarr API connectivity and authentication
2. **Show Iteration**: Loops through all TV shows in the Plex "TV Shows" library
3. **Exclusion Check**: Skips shows that are in the `exclude_shows` list
4. **Season Analysis**: For each show:
- Gets all seasons sorted by index
- Checks if there are multiple seasons
- Examines the latest season's episodes
5. **Condition Check**: If the last 3 episodes of the latest season are watched:
- Retrieves the show's TVDB ID from Plex metadata
- Finds the corresponding series in Sonarr
- Marks all previous seasons as unmonitored in Sonarr
- Optionally deletes previous seasons from Plex (currently commented out)
### Excluded Shows
The script maintains a list of shows that will be skipped during processing:
- Stargate SG-1
- Space Sheriff Gavan
- Spider-Man and His Amazing Friends
- Super Sentai
- Superman & Lois
- UFO Robot Grendizer
- Zorro
- Saved by the Bell: The College Years
- Saber Rider and the Star Sheriffs
- Prison Break
- Power Rangers
- The Outer Limits (1995)
- MacGyver
- Knight Rider
- Chuck
- Breaking Bad
- Amazing Stories (1985)
- Airwolf
- The Adventures of Superboy (1988)
## Usage
### Basic Usage
```bash
python plexsonarr.py
```
### Manual Exclude List Processing
To manually unmonitor all shows in the exclude list, uncomment this line in the script:
```python
unmonitor_all_excluded_shows()
```
### Enable Season Deletion
To actually delete seasons from Plex (currently disabled for safety), uncomment this line:
```python
season.delete()
```
## Functions
### Core Functions
- `verify_sonarr_api()`: Verifies Sonarr API connectivity
- `get_tvdb_id(show)`: Extracts TVDB ID from Plex show metadata
- `get_series_id_from_tvdb(tvdb_id)`: Gets Sonarr series ID from TVDB ID
- `mark_season_unmonitored(series_id, season_number)`: Marks a season as unmonitored in Sonarr
- `last_3_episodes_watched(season)`: Checks if the last 3 episodes of a season are watched
- `unmonitor_all_excluded_shows()`: Processes all shows in the exclude list
## Safety Features
- **Dry Run Mode**: Season deletion is commented out by default
- **API Verification**: Checks Sonarr API before processing
- **Exclude List**: Protects specific shows from processing
- **Latest Season Protection**: Never processes the latest season for deletion
- **Detailed Logging**: Comprehensive output for monitoring operations
## Error Handling
The script includes error handling for:
- Sonarr API connectivity issues
- Missing TVDB IDs in Plex metadata
- Series not found in Sonarr
- HTTP request failures
## Logging Output
The script provides detailed console output including:
- Show titles being processed
- TVDB IDs found
- Sonarr series IDs
- Seasons being marked as unmonitored
- Watched episodes in the latest season
- API verification status
## Important Notes
⚠️ **Warning**: This script can permanently delete media files from your Plex server. Always test thoroughly in a safe environment before running on production data.
- The season deletion functionality is disabled by default (commented out)
- Make sure your Plex and Sonarr instances are properly backed up
- Test with a small subset of shows before running on your entire library
- Monitor the output carefully during initial runs
## Troubleshooting
### Common Issues
1. **API Connection Failures**: Check that your server URLs and API keys are correct
2. **Missing TVDB IDs**: Some shows may not have TVDB metadata in Plex
3. **Series Not Found**: Shows may exist in Plex but not in Sonarr
4. **Permission Errors**: Ensure the script has appropriate network access
### Debug Mode
For additional debugging, uncomment the print statements in the `mark_season_unmonitored` function to see detailed HTTP response information.
## License
This script is provided as-is for educational and personal use. Use at your own risk.
+77 -7
View File
@@ -1,7 +1,7 @@
from plexapi.server import PlexServer from plexapi.server import PlexServer
import requests import requests
PLEX_TOKEN = "uZn42JMVkQpyb_duFsvT" PLEX_TOKEN = "gsyJxRUczrTPsyvopP6k"
PLEX_SERVER_URL = "http://192.168.50.111:32400" PLEX_SERVER_URL = "http://192.168.50.111:32400"
SONARR_API_KEY = "2537de37fded4874ae83da9cf3c14f34" SONARR_API_KEY = "2537de37fded4874ae83da9cf3c14f34"
SONARR_SERVER_URL = "http://192.168.50.111:8989" SONARR_SERVER_URL = "http://192.168.50.111:8989"
@@ -12,7 +12,7 @@ tv_shows = plex.library.section('TV Shows')
# List of TV show titles to exclude # List of TV show titles to exclude
exclude_shows = ["Stargate SG-1", "Space Sheriff Gavan", "Spider-Man and His Amazing Friends","Super Sentai", "Superman & Lois", "UFO Robot Grendizer","Zorro", exclude_shows = ["Stargate SG-1", "Space Sheriff Gavan", "Spider-Man and His Amazing Friends","Super Sentai", "Superman & Lois", "UFO Robot Grendizer","Zorro",
"Saved by the Bell: The College Years","Saber Rider and the Star Sheriffs","Prison Break","Power Rangers","The Outer Limits (1995)","MacGyver","Knight Rider", "Saved by the Bell: The College Years","Saber Rider and the Star Sheriffs","Prison Break","Power Rangers","The Outer Limits (1995)","MacGyver","Knight Rider",
"Chuck","Breaking Bad","Amazing Stories (1985)","Airwolf","The Adventures of Superboy (1988)"] "Chuck","Breaking Bad","Amazing Stories (1985)","Airwolf","The Adventures of Superboy (1988)", "The Amazing Race"]
def unmonitor_all_excluded_shows(): def unmonitor_all_excluded_shows():
for show_title in exclude_shows: for show_title in exclude_shows:
@@ -31,6 +31,48 @@ def last_3_episodes_watched(season):
episodes = sorted(season.episodes(), key=lambda ep: ep.index) episodes = sorted(season.episodes(), key=lambda ep: ep.index)
return all(ep.isWatched for ep in episodes[-3:]) return all(ep.isWatched for ep in episodes[-3:])
# Function to check if the last 5 episodes of a show are watched, if not unmonitor the show
def check_last_5_episodes_and_unmonitor(show):
"""
Check if the last 5 episodes of a show are watched.
If not, mark the entire show as unmonitored in Sonarr.
"""
try:
# Get all episodes from all seasons
all_episodes = []
for season in show.seasons():
all_episodes.extend(season.episodes())
# Sort episodes by air date or episode index
all_episodes = sorted(all_episodes, key=lambda ep: (ep.seasonNumber, ep.index))
if len(all_episodes) < 5:
print(f" ⚠️ Show has less than 5 episodes total. Skipping check.")
return True
# Get the last 5 episodes
last_5_episodes = all_episodes[-5:]
watched_count = sum(1 for ep in last_5_episodes if ep.isWatched)
print(f" 📊 Last 5 episodes: {watched_count}/5 watched")
if watched_count >= 1:
print(f" ✅ At least 1 of last 5 episodes watched. Continuing normal processing...")
return True
elif watched_count == 0:
print(f" ❌ None of last 5 episodes watched. Marking show as unmonitored...")
tvdb_id = get_tvdb_id(show)
series_id = get_series_id_from_tvdb(tvdb_id)
mark_show_unmonitored(series_id)
return False
else:
print(f" ✅ Show remains monitored.")
return True
except Exception as e:
print(f" ❌ Error checking episodes: {e}")
return True
# Function to check if Sonarr API is alive and the token is correct # Function to check if Sonarr API is alive and the token is correct
def verify_sonarr_api(): def verify_sonarr_api():
url = f"{SONARR_SERVER_URL}/api/v3/system/status?apikey={SONARR_API_KEY}" url = f"{SONARR_SERVER_URL}/api/v3/system/status?apikey={SONARR_API_KEY}"
@@ -81,13 +123,35 @@ def mark_season_unmonitored(series_id, season_number):
for season in series['seasons']: for season in series['seasons']:
if season['seasonNumber'] == season_number: if season['seasonNumber'] == season_number:
if season['monitored'] == False:
# print(f"Season {season_number} is already unmonitored for series ID {series_id}. Skipping.")
return
season['monitored'] = False season['monitored'] = False
break
response = requests.put(url, json=series) response = requests.put(url, json=series)
# print(f"PUT Response Status Code: {response.status_code}") # print(f"PUT Response Status Code: {response.status_code}")
# print(f"PUT Response Content: {response.content}") # print(f"PUT Response Content: {response.content}")
response.raise_for_status() response.raise_for_status()
print(f"Season {season_number} marked as unmonitored for series ID {series_id}.") # print(f"Season {season_number} marked as unmonitored for series ID {series_id}.")
def mark_show_unmonitored(series_id):
"""Mark an entire show as unmonitored in Sonarr"""
url = f"{SONARR_SERVER_URL}/api/v3/series/{series_id}?apikey={SONARR_API_KEY}"
response = requests.get(url)
response.raise_for_status()
series = response.json()
if series['monitored'] == False:
print(f" ️ Show is already unmonitored in Sonarr. Skipping.")
return
# Mark the entire series as unmonitored
series['monitored'] = False
response = requests.put(url, json=series)
response.raise_for_status()
print(f" ✅ Show marked as unmonitored in Sonarr (Series ID: {series_id})")
def get_tvdb_id(show): def get_tvdb_id(show):
@@ -107,13 +171,19 @@ for show in tv_shows.all():
if show.title not in exclude_shows: if show.title not in exclude_shows:
#print(f"TV Show: {show}") #print(f"TV Show: {show}")
print(f"Title: {show.title}") print(f"\n📺 {show.title}")
#print(f"Year: {show.year}") print(f" 📅 Year: {show.year if show.year else 'Unknown'}")
#print(f"Rating: {show.rating}") print(f" ⭐ Rating: {show.rating if show.rating else 'N/A'}/10")
print(f" 🎬 Seasons: {show.childCount}")
print(" " + "" * 40)
# Check if last 5 episodes are watched, if not unmonitor the show
if not check_last_5_episodes_and_unmonitor(show):
continue # Skip further processing if show was unmonitored
#print(f"Summary: {show.summary}") #print(f"Summary: {show.summary}")
#print(f"Studio: {show.studio}") #print(f"Studio: {show.studio}")
# print(f"Actors: {', '.join(actor.tag for actor in show.actors)}") # print(f"Actors: {', '.join(actor.tag for actor in show.actors)}")
#print(f"Seasons: {show.childCount}")
#print(f"Views: {show.viewCount}") #print(f"Views: {show.viewCount}")
#print(f"Guid: {show.guid}") #print(f"Guid: {show.guid}")
#print("="*40) #print("="*40)
+1
View File
@@ -0,0 +1 @@
pytest
+4
View File
@@ -0,0 +1,4 @@
import pytest
def test_verify_sonarr_api():
assert verify_sonarr_api() == expected_result
+6
View File
@@ -0,0 +1,6 @@
[tool:pytest]
testpaths = .
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=short
+5
View File
@@ -0,0 +1,5 @@
plexapi
requests
pytest
pytest-mock
requests-mock
+116
View File
@@ -0,0 +1,116 @@
"""
Standalone script to unmonitor all excluded shows in Sonarr
"""
from plexapi.server import PlexServer
import requests
# Configuration
PLEX_TOKEN = "uZn42JMVkQpyb_duFsvT"
PLEX_SERVER_URL = "http://192.168.50.111:32400"
SONARR_API_KEY = "2537de37fded4874ae83da9cf3c14f34"
SONARR_SERVER_URL = "http://192.168.50.111:8989"
# Initialize Plex connection
plex = PlexServer(PLEX_SERVER_URL, PLEX_TOKEN)
tv_shows = plex.library.section('TV Shows')
# List of TV show titles to exclude
exclude_shows = ["Stargate SG-1", "Space Sheriff Gavan", "Spider-Man and His Amazing Friends","Super Sentai", "Superman & Lois", "UFO Robot Grendizer","Zorro",
"Saved by the Bell: The College Years","Saber Rider and the Star Sheriffs","Prison Break","Power Rangers","The Outer Limits (1995)","MacGyver","Knight Rider",
"Chuck","Breaking Bad","Amazing Stories (1985)","Airwolf","The Adventures of Superboy (1988)"]
def verify_sonarr_api():
"""Check if Sonarr API is alive and the token is correct"""
url = f"{SONARR_SERVER_URL}/api/v3/system/status?apikey={SONARR_API_KEY}"
try:
response = requests.get(url)
response.raise_for_status()
print("✅ Sonarr API is alive and the token is correct.")
return True
except requests.exceptions.RequestException as e:
print(f"❌ Failed to verify Sonarr API: {e}")
return False
def get_tvdb_id(show):
"""Extract TVDB ID from Plex show metadata"""
for guid in reversed(show.guids):
if guid.id.startswith("tvdb"):
return guid.id.split("://")[1]
raise ValueError(f"No TVDB ID found for show: {show.title}")
def get_series_id_from_tvdb(tvdb_id):
"""Get Sonarr series ID from TVDB ID"""
url = f"{SONARR_SERVER_URL}/api/v3/series/lookup?term=tvdb:{tvdb_id}&apikey={SONARR_API_KEY}"
response = requests.get(url)
response.raise_for_status()
series = response.json()
if series:
return series[0]['id']
else:
raise ValueError(f"No series found for TVDB ID: {tvdb_id}")
def mark_season_unmonitored(series_id, season_number):
"""Mark a season as unmonitored in Sonarr"""
url = f"{SONARR_SERVER_URL}/api/v3/series/{series_id}?apikey={SONARR_API_KEY}"
response = requests.get(url)
response.raise_for_status()
series = response.json()
for season in series['seasons']:
if season['seasonNumber'] == season_number:
season['monitored'] = False
response = requests.put(url, json=series)
response.raise_for_status()
print(f" ✅ Season {season_number} marked as unmonitored for series ID {series_id}")
def unmonitor_all_excluded_shows():
"""Unmonitor all seasons of shows in the exclude list"""
print("🚀 Starting to unmonitor all excluded shows...")
print("=" * 60)
success_count = 0
error_count = 0
for show_title in exclude_shows:
try:
print(f"\n📺 Processing: {show_title}")
show = tv_shows.get(title=show_title)
print(f" Found in Plex: {show.title}")
tvdb_id = get_tvdb_id(show)
print(f" TVDB ID: {tvdb_id}")
series_id = get_series_id_from_tvdb(tvdb_id)
print(f" Sonarr Series ID: {series_id}")
seasons = show.seasons()
print(f" Found {len(seasons)} seasons")
for season in seasons:
mark_season_unmonitored(series_id, season.index)
print(f"✅ All seasons of '{show_title}' have been marked as unmonitored.")
success_count += 1
except Exception as e:
print(f"❌ Error processing '{show_title}': {e}")
error_count += 1
print("\n" + "=" * 60)
print(f"🎉 Processing complete!")
print(f"✅ Successfully processed: {success_count} shows")
print(f"❌ Errors encountered: {error_count} shows")
if __name__ == "__main__":
print("🔍 Verifying Sonarr API connection...")
if verify_sonarr_api():
print("\n🎯 Ready to unmonitor excluded shows")
user_input = input("Do you want to proceed? (y/n): ").lower().strip()
if user_input in ['y', 'yes']:
unmonitor_all_excluded_shows()
else:
print("❌ Operation cancelled by user.")
else:
print("❌ Cannot proceed - Sonarr API verification failed.")
+157
View File
@@ -0,0 +1,157 @@
import pytest
import requests
import requests_mock
from unittest.mock import patch, MagicMock
import sys
import os
# Add the current directory to the path to import plexsonarr
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# Import the function we want to test
# We need to mock the Plex connection since we're only testing the API verification
with patch('plexapi.server.PlexServer'), patch('plexsonarr.tv_shows'):
import plexsonarr
class TestVerifySonarrApi:
"""Test cases for verify_sonarr_api function"""
def test_verify_sonarr_api_success(self, capsys):
"""Test successful API verification"""
with requests_mock.Mocker() as m:
# Mock successful response
expected_url = f"{plexsonarr.SONARR_SERVER_URL}/api/v3/system/status?apikey={plexsonarr.SONARR_API_KEY}"
m.get(expected_url, json={'status': 'ok'}, status_code=200)
# Call the function
plexsonarr.verify_sonarr_api()
# Check that the success message was printed
captured = capsys.readouterr()
assert "Sonarr API is alive and the token is correct." in captured.out
def test_verify_sonarr_api_http_error(self):
"""Test API verification with HTTP error (e.g., 401 Unauthorized)"""
with requests_mock.Mocker() as m:
expected_url = f"{plexsonarr.SONARR_SERVER_URL}/api/v3/system/status?apikey={plexsonarr.SONARR_API_KEY}"
m.get(expected_url, status_code=401, text="Unauthorized")
# Should raise an exception
with pytest.raises(requests.exceptions.HTTPError):
plexsonarr.verify_sonarr_api()
def test_verify_sonarr_api_connection_error(self, capsys):
"""Test API verification with connection error"""
with requests_mock.Mocker() as m:
expected_url = f"{plexsonarr.SONARR_SERVER_URL}/api/v3/system/status?apikey={plexsonarr.SONARR_API_KEY}"
m.get(expected_url, exc=requests.exceptions.ConnectionError("Connection failed"))
# Should raise an exception and print error message
with pytest.raises(requests.exceptions.ConnectionError):
plexsonarr.verify_sonarr_api()
captured = capsys.readouterr()
assert "Failed to verify Sonarr API:" in captured.out
def test_verify_sonarr_api_timeout(self):
"""Test API verification with timeout"""
with requests_mock.Mocker() as m:
expected_url = f"{plexsonarr.SONARR_SERVER_URL}/api/v3/system/status?apikey={plexsonarr.SONARR_API_KEY}"
m.get(expected_url, exc=requests.exceptions.Timeout("Request timeout"))
with pytest.raises(requests.exceptions.Timeout):
plexsonarr.verify_sonarr_api()
def test_verify_sonarr_api_invalid_json(self):
"""Test API verification with invalid JSON response"""
with requests_mock.Mocker() as m:
expected_url = f"{plexsonarr.SONARR_SERVER_URL}/api/v3/system/status?apikey={plexsonarr.SONARR_API_KEY}"
m.get(expected_url, text="Invalid JSON", status_code=200)
# Should still succeed as we only check status code
plexsonarr.verify_sonarr_api()
def test_verify_sonarr_api_404_not_found(self):
"""Test API verification with 404 Not Found"""
with requests_mock.Mocker() as m:
expected_url = f"{plexsonarr.SONARR_SERVER_URL}/api/v3/system/status?apikey={plexsonarr.SONARR_API_KEY}"
m.get(expected_url, status_code=404, text="Not Found")
with pytest.raises(requests.exceptions.HTTPError):
plexsonarr.verify_sonarr_api()
def test_verify_sonarr_api_500_server_error(self):
"""Test API verification with 500 Server Error"""
with requests_mock.Mocker() as m:
expected_url = f"{plexsonarr.SONARR_SERVER_URL}/api/v3/system/status?apikey={plexsonarr.SONARR_API_KEY}"
m.get(expected_url, status_code=500, text="Internal Server Error")
with pytest.raises(requests.exceptions.HTTPError):
plexsonarr.verify_sonarr_api()
class TestVerifySonarrApiStandalone:
"""Test the verify_sonarr_api function in isolation without importing the full module"""
@patch('requests.get')
def test_verify_sonarr_api_standalone_success(self, mock_get, capsys):
"""Test the function logic directly with mocked requests"""
# Setup
mock_response = MagicMock()
mock_response.raise_for_status.return_value = None
mock_get.return_value = mock_response
# Define the function locally for testing
def verify_sonarr_api():
SONARR_SERVER_URL = "http://test:8989"
SONARR_API_KEY = "test_key"
url = f"{SONARR_SERVER_URL}/api/v3/system/status?apikey={SONARR_API_KEY}"
try:
response = requests.get(url)
response.raise_for_status()
print("Sonarr API is alive and the token is correct.")
except requests.exceptions.RequestException as e:
print(f"Failed to verify Sonarr API: {e}")
raise
# Execute
verify_sonarr_api()
# Verify
mock_get.assert_called_once_with("http://test:8989/api/v3/system/status?apikey=test_key")
mock_response.raise_for_status.assert_called_once()
captured = capsys.readouterr()
assert "Sonarr API is alive and the token is correct." in captured.out
@patch('requests.get')
def test_verify_sonarr_api_standalone_failure(self, mock_get, capsys):
"""Test the function logic with mocked failure"""
# Setup
mock_get.side_effect = requests.exceptions.ConnectionError("Connection failed")
# Define the function locally for testing
def verify_sonarr_api():
SONARR_SERVER_URL = "http://test:8989"
SONARR_API_KEY = "test_key"
url = f"{SONARR_SERVER_URL}/api/v3/system/status?apikey={SONARR_API_KEY}"
try:
response = requests.get(url)
response.raise_for_status()
print("Sonarr API is alive and the token is correct.")
except requests.exceptions.RequestException as e:
print(f"Failed to verify Sonarr API: {e}")
raise
# Execute and verify exception
with pytest.raises(requests.exceptions.ConnectionError):
verify_sonarr_api()
captured = capsys.readouterr()
assert "Failed to verify Sonarr API:" in captured.out
if __name__ == "__main__":
# Run specific tests
pytest.main([__file__, "-v"])
+117
View File
@@ -0,0 +1,117 @@
"""
Simple test script for verify_sonarr_api function
Run this script to manually test the API verification
"""
import requests
from unittest.mock import patch, MagicMock
# Configuration (same as in your main script)
SONARR_API_KEY = "2537de37fded4874ae83da9cf3c14f34"
SONARR_SERVER_URL = "http://192.168.50.111:8989"
def verify_sonarr_api():
"""Copy of the function from plexsonarr.py for testing"""
url = f"{SONARR_SERVER_URL}/api/v3/system/status?apikey={SONARR_API_KEY}"
try:
response = requests.get(url)
response.raise_for_status()
print("✅ Sonarr API is alive and the token is correct.")
return True
except requests.exceptions.RequestException as e:
print(f"❌ Failed to verify Sonarr API: {e}")
return False
def test_with_mock_success():
"""Test with a mocked successful response"""
print("\n🧪 Testing with mocked SUCCESS response...")
with patch('requests.get') as mock_get:
# Setup mock
mock_response = MagicMock()
mock_response.raise_for_status.return_value = None
mock_response.status_code = 200
mock_get.return_value = mock_response
# Test
result = verify_sonarr_api()
# Verify
assert result == True
mock_get.assert_called_once()
print("✅ Mock success test passed!")
def test_with_mock_failure():
"""Test with a mocked failure response"""
print("\n🧪 Testing with mocked FAILURE response...")
with patch('requests.get') as mock_get:
# Setup mock to raise an exception
mock_get.side_effect = requests.exceptions.ConnectionError("Mocked connection error")
# Test
result = verify_sonarr_api()
# Verify
assert result == False
print("✅ Mock failure test passed!")
def test_with_mock_http_error():
"""Test with a mocked HTTP error (401 Unauthorized)"""
print("\n🧪 Testing with mocked HTTP ERROR response...")
with patch('requests.get') as mock_get:
# Setup mock
mock_response = MagicMock()
mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("401 Unauthorized")
mock_response.status_code = 401
mock_get.return_value = mock_response
# Test
result = verify_sonarr_api()
# Verify
assert result == False
print("✅ Mock HTTP error test passed!")
def test_real_api():
"""Test with real API call"""
print("\n🌐 Testing with REAL API call...")
print(f"📡 Calling: {SONARR_SERVER_URL}/api/v3/system/status")
result = verify_sonarr_api()
if result:
print("✅ Real API test passed! Your Sonarr API is working.")
else:
print("❌ Real API test failed. Check your configuration.")
return result
if __name__ == "__main__":
print("🚀 Starting verify_sonarr_api tests...")
print("=" * 50)
# Run mock tests
test_with_mock_success()
test_with_mock_failure()
test_with_mock_http_error()
# Ask user if they want to test real API
print("\n" + "=" * 50)
user_input = input("Do you want to test the real API? (y/n): ").lower().strip()
if user_input in ['y', 'yes']:
success = test_real_api()
if success:
print("\n🎉 All tests completed successfully!")
else:
print("\n💡 Tip: Check your SONARR_SERVER_URL and SONARR_API_KEY configuration")
else:
print("\n✅ Mock tests completed successfully!")
print("\n📋 Test Summary:")
print("- Mock success test: ✅")
print("- Mock failure test: ✅")
print("- Mock HTTP error test: ✅")
if user_input in ['y', 'yes']:
print(f"- Real API test: {'' if success else ''}")