1
0

Initial Commit

This commit is contained in:
2026-01-15 14:43:27 -05:00
commit c5203fcfbb
2 changed files with 400 additions and 0 deletions

120
README.md Normal file
View File

@@ -0,0 +1,120 @@
# Emby Live TV Channel Logo Updater
Automatically copy or clear Live TV channel logos in Emby Server.
## What It Does
- **Copy Mode** (default): Copies the Primary logo (Logo Dark Version) to LogoLight (Logo Light Version) and LogoLightColor (Logo Light with Colour) for all TV channels
- **Clear Mode** (`--clear`): Removes all logos (Primary, LogoLight, LogoLightColor) from channels
## Requirements
- Python 3.6+
- Emby Server with API access
- API key from Emby Server
## Getting Your API Key
1. Log into Emby Server as an administrator
2. Go to Settings → Advanced → API Keys
3. Create a new API key or use an existing one
## Usage
### Copy Logos (Default Mode)
**Dry run** (see what would happen without making changes):
```bash
python update_channel_logos.py \
--server https://your-emby-server.com \
--api-key YOUR_API_KEY
```
**Execute** (actually make changes):
```bash
python update_channel_logos.py \
--server https://your-emby-server.com \
--api-key YOUR_API_KEY \
--execute
```
**Test on first channel only**:
```bash
python update_channel_logos.py \
--server https://your-emby-server.com \
--api-key YOUR_API_KEY \
--first-only \
--execute
```
### Clear All Logos
**Dry run**:
```bash
python update_channel_logos.py \
--server https://your-emby-server.com \
--api-key YOUR_API_KEY \
--clear
```
**Execute**:
```bash
python update_channel_logos.py \
--server https://your-emby-server.com \
--api-key YOUR_API_KEY \
--clear \
--execute
```
## Command-Line Options
| Option | Description |
|--------|-------------|
| `--server` | **Required.** Emby server URL (e.g., `https://emby.example.com`) |
| `--api-key` | **Required.** Your Emby API key |
| `--execute` | Actually perform the changes (without this, runs in dry-run mode) |
| `--first-only` | Only process the first channel (useful for testing) |
| `--clear` | Clear all logos instead of copying them |
## Examples
**Recommended workflow:**
1. Test on first channel with dry run:
```bash
python update_channel_logos.py --server URL --api-key KEY --first-only
```
2. Execute on first channel:
```bash
python update_channel_logos.py --server URL --api-key KEY --first-only --execute
```
3. If successful, run on all channels:
```bash
python update_channel_logos.py --server URL --api-key KEY --execute
```
## How It Works
1. Connects to your Emby server
2. Fetches all Live TV channels
3. For each channel:
- **Copy mode**: Downloads the Primary logo and uploads it as LogoLight and LogoLightColor
- **Clear mode**: Deletes Primary, LogoLight, and LogoLightColor
4. Skips channels that already have the logos (copy mode) or have no logos (clear mode)
5. Shows a summary of results
## Safety Features
- **Dry run by default**: Always runs in simulation mode unless `--execute` is specified
- **Confirmation prompt**: Asks for confirmation before executing changes
- **First-only testing**: Test on a single channel before processing all channels
- **Skip existing**: In copy mode, skips channels that already have both light logos
## Notes
- The script only affects Live TV channels, not other media
- Changes may take a few moments to appear in the Emby interface
- You may need to refresh your browser/app to see changes
- Use `--clear` with caution - it deletes logos from all channels

280
update_channel_logos.py Normal file
View File

@@ -0,0 +1,280 @@
#!/usr/bin/env python3
"""
Emby Live TV Channel Logo Updater
Copies 'Logo (Dark Version)' to 'Logo (Light Version)' and 'Logo (Light with Colour)'.
"""
import argparse
import base64
import json
import sys
from typing import Dict, List, Optional
import urllib.request
import urllib.error
import urllib.parse
class EmbyClient:
"""Client for interacting with the Emby Server API."""
def __init__(self, server_url: str, api_key: str):
self.server_url = server_url.rstrip('/')
self.api_key = api_key
def _make_request(self, endpoint: str, method: str = 'GET', data: Optional[Dict] = None) -> Dict:
url = f"{self.server_url}{endpoint}"
if method == 'GET':
separator = '&' if '?' in url else '?'
url = f"{url}{separator}api_key={self.api_key}"
request = urllib.request.Request(url, method=method)
request.add_header('X-Emby-Token', self.api_key)
if data is not None:
request.add_header('Content-Type', 'application/json')
request.data = json.dumps(data).encode('utf-8')
try:
with urllib.request.urlopen(request) as response:
if response.status == 204:
return {}
return json.loads(response.read().decode('utf-8'))
except urllib.error.HTTPError as e:
error_body = e.read().decode('utf-8') if e.fp else 'No error details'
raise Exception(f"HTTP {e.code}: {error_body}")
def get_live_tv_channels(self) -> List[Dict]:
response = self._make_request('/LiveTv/Channels?Type=TV&EnableImages=true')
return response.get('Items', [])
def get_channel_by_id(self, channel_id: str) -> Dict:
return self._make_request(f'/LiveTv/Channels/{channel_id}')
def download_image(self, item_id: str, image_type: str) -> bytes:
url = f"{self.server_url}/Items/{item_id}/Images/{image_type}?api_key={self.api_key}"
with urllib.request.urlopen(urllib.request.Request(url)) as response:
return response.read()
def upload_image(self, item_id: str, image_type: str, image_data: bytes) -> None:
if image_data.startswith(b'\x89PNG'):
content_type = 'image/png'
elif image_data.startswith(b'\xff\xd8'):
content_type = 'image/jpeg'
elif image_data.startswith(b'GIF'):
content_type = 'image/gif'
else:
content_type = 'application/octet-stream'
url = f"{self.server_url}/Items/{item_id}/Images/{image_type}"
request = urllib.request.Request(url, method='POST')
request.add_header('X-Emby-Token', self.api_key)
request.add_header('Content-Type', content_type)
request.data = base64.b64encode(image_data).decode('utf-8').encode('utf-8')
try:
with urllib.request.urlopen(request):
pass
except urllib.error.HTTPError as e:
error_body = e.read().decode('utf-8') if e.fp else 'No error details'
raise Exception(f"Failed to upload {image_type}: HTTP {e.code} - {error_body}")
def delete_image(self, item_id: str, image_type: str) -> None:
url = f"{self.server_url}/Items/{item_id}/Images/{image_type}"
request = urllib.request.Request(url, method='DELETE')
request.add_header('X-Emby-Token', self.api_key)
try:
with urllib.request.urlopen(request):
pass
except urllib.error.HTTPError as e:
error_body = e.read().decode('utf-8') if e.fp else 'No error details'
raise Exception(f"Failed to delete {image_type}: HTTP {e.code} - {error_body}")
def copy_primary_to_light_logos(client: EmbyClient, channel_id: str, channel_name: str, dry_run: bool = True) -> bool:
try:
channel_data = client.get_channel_by_id(channel_id)
image_tags = channel_data.get('ImageTags', {})
if not image_tags.get('Primary'):
print(f" ⚠️ No Primary image, skipping")
return False
has_logo_light = 'LogoLight' in image_tags
has_logo_light_color = 'LogoLightColor' in image_tags
if has_logo_light and has_logo_light_color:
print(f" ✓ Already has both logos")
return True
missing = []
if not has_logo_light:
missing.append('LogoLight')
if not has_logo_light_color:
missing.append('LogoLightColor')
if dry_run:
print(f" → Would upload: {', '.join(missing)}")
return True
print(f" 📥 Downloading Primary...")
primary_image_data = client.download_image(channel_id, 'Primary')
if not has_logo_light:
print(f" 📤 Uploading LogoLight...")
client.upload_image(channel_id, 'LogoLight', primary_image_data)
if not has_logo_light_color:
print(f" 📤 Uploading LogoLightColor...")
client.upload_image(channel_id, 'LogoLightColor', primary_image_data)
print(f" ✓ Successfully uploaded")
return True
except Exception as e:
print(f" ✗ Error: {e}")
import traceback
traceback.print_exc()
return False
def clear_channel_logos(client: EmbyClient, channel_id: str, channel_name: str, dry_run: bool = True) -> bool:
try:
channel_data = client.get_channel_by_id(channel_id)
image_tags = channel_data.get('ImageTags', {})
image_types = ['Primary', 'LogoLight', 'LogoLightColor']
existing = [img_type for img_type in image_types if img_type in image_tags]
if not existing:
print(f" No logos to clear")
return True
if dry_run:
print(f" → Would delete: {', '.join(existing)}")
return True
for img_type in existing:
print(f" 🗑️ Deleting {img_type}...")
client.delete_image(channel_id, img_type)
print(f" ✓ Successfully cleared {len(existing)} logo(s)")
return True
except Exception as e:
print(f" ✗ Error: {e}")
import traceback
traceback.print_exc()
return False
def update_channel_logos(client: EmbyClient, dry_run: bool = True, first_only: bool = False) -> None:
channels = client.get_live_tv_channels()
channels_to_process = [channels[0]] if first_only else channels
print("\n" + "=" * 80)
if first_only:
print(f"{'DRY RUN: TESTING ON' if dry_run else 'PROCESSING'} FIRST CHANNEL ONLY")
else:
print(f"{'DRY RUN: SIMULATING' if dry_run else 'PROCESSING'} ALL {len(channels)} CHANNELS")
print("=" * 80)
success_count = 0
skip_count = 0
for i, channel in enumerate(channels_to_process, 1):
print(f"\n[{i}/{len(channels_to_process)}] {channel.get('Name', 'Unknown')}")
if copy_primary_to_light_logos(client, channel['Id'], channel.get('Name'), dry_run):
success_count += 1
else:
skip_count += 1
print("\n" + "=" * 80)
print("SUMMARY")
print("=" * 80)
print(f"Total: {len(channels_to_process)}")
print(f"{'Would update' if dry_run else 'Updated'}: {success_count}")
print(f"Skipped: {skip_count}")
if dry_run:
print("\n💡 Dry run. Use --execute to perform actual updates.")
if first_only and not dry_run:
print("\n💡 Processed first channel only. Remove --first-only to process all.")
def clear_logos(client: EmbyClient, dry_run: bool = True, first_only: bool = False) -> None:
channels = client.get_live_tv_channels()
channels_to_process = [channels[0]] if first_only else channels
print("\n" + "=" * 80)
if first_only:
print(f"{'DRY RUN: CLEARING' if dry_run else 'CLEARING'} FIRST CHANNEL ONLY")
else:
print(f"{'DRY RUN: CLEARING' if dry_run else 'CLEARING'} ALL {len(channels)} CHANNELS")
print("=" * 80)
success_count = 0
skip_count = 0
for i, channel in enumerate(channels_to_process, 1):
print(f"\n[{i}/{len(channels_to_process)}] {channel.get('Name', 'Unknown')}")
if clear_channel_logos(client, channel['Id'], channel.get('Name'), dry_run):
success_count += 1
else:
skip_count += 1
print("\n" + "=" * 80)
print("SUMMARY")
print("=" * 80)
print(f"Total: {len(channels_to_process)}")
print(f"{'Would clear' if dry_run else 'Cleared'}: {success_count}")
print(f"Skipped: {skip_count}")
if dry_run:
print("\n💡 Dry run. Use --execute to perform actual deletions.")
if first_only and not dry_run:
print("\n💡 Processed first channel only. Remove --first-only to process all.")
def main():
parser = argparse.ArgumentParser(description='Copy Emby Live TV channel dark logos to light versions')
parser.add_argument('--server', required=True, help='Emby server URL')
parser.add_argument('--api-key', required=True, help='Emby API key')
parser.add_argument('--execute', action='store_true', help='Actually perform updates')
parser.add_argument('--first-only', action='store_true', help='Only process first channel')
parser.add_argument('--clear', action='store_true', help='Clear all logos (Primary, LogoLight, LogoLightColor)')
args = parser.parse_args()
client = EmbyClient(args.server, args.api_key)
try:
print(f"Connecting to {args.server}...")
channels = client.get_live_tv_channels()
print(f"✓ Connected! Found {len(channels)} TV channels")
except Exception as e:
print(f"✗ Connection failed: {e}", file=sys.stderr)
sys.exit(1)
if args.clear:
if args.execute:
target = "first channel" if args.first_only else "all channels"
if input(f"\nCLEAR ALL LOGOS from {target}? (yes/no): ").lower() == 'yes':
clear_logos(client, dry_run=False, first_only=args.first_only)
else:
print("Cancelled.")
else:
clear_logos(client, dry_run=True, first_only=args.first_only)
else:
if args.execute:
target = "first channel" if args.first_only else "all channels"
if input(f"\nModify {target}? (yes/no): ").lower() == 'yes':
update_channel_logos(client, dry_run=False, first_only=args.first_only)
else:
print("Cancelled.")
else:
update_channel_logos(client, dry_run=True, first_only=args.first_only)
if __name__ == '__main__':
main()