Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 487b28e682 | |||
| 5413d621d4 | |||
| 66e40ff077 |
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"python.testing.unittestArgs": [
|
||||||
|
"-v",
|
||||||
|
"-s",
|
||||||
|
".",
|
||||||
|
"-p",
|
||||||
|
"*_test.py"
|
||||||
|
],
|
||||||
|
"python.testing.pytestEnabled": false,
|
||||||
|
"python.testing.unittestEnabled": true
|
||||||
|
}
|
||||||
+219
@@ -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.
Binary file not shown.
@@ -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.") """
|
||||||
@@ -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
@@ -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.
|
||||||
+78
-8
@@ -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)
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
pytest
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
def test_verify_sonarr_api():
|
||||||
|
assert verify_sonarr_api() == expected_result
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[tool:pytest]
|
||||||
|
testpaths = .
|
||||||
|
python_files = test_*.py
|
||||||
|
python_classes = Test*
|
||||||
|
python_functions = test_*
|
||||||
|
addopts = -v --tb=short
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
plexapi
|
||||||
|
requests
|
||||||
|
pytest
|
||||||
|
pytest-mock
|
||||||
|
requests-mock
|
||||||
@@ -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.")
|
||||||
@@ -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"])
|
||||||
@@ -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 '❌'}")
|
||||||
Reference in New Issue
Block a user