1
0

containerize & enhance filtering
All checks were successful
Build & Push Docker Image / build-and-publish (push) Successful in 11s

This commit is contained in:
2026-02-08 11:58:22 -05:00
parent 25ef2adf71
commit 3efdc7b7bc
8 changed files with 518 additions and 13 deletions

31
.dockerignore Normal file
View File

@@ -0,0 +1,31 @@
# Git files
.git
.gitignore
.gitea
# Environment files
.env
.env.*
!.env.example
# Python cache
__pycache__
*.py[cod]
*$py.class
*.so
.Python
# Documentation
*.md
!README.md
# IDE
.vscode
.idea
*.swp
*.swo
*~
# OS files
.DS_Store
Thumbs.db

36
.env.example Normal file
View File

@@ -0,0 +1,36 @@
# Copy to .env: cp .env.example .env
# Required - Emby Server Configuration
EMBY_SERVER_URL=https://your-emby-server.com
EMBY_API_KEY=your_api_key_here
# Mode: copy (default) or clear
MODE=copy
# Safety: true = dry run, false = execute
DRY_RUN=true
# Test on first channel only
FIRST_ONLY=false
# Overwrite existing logos (copy mode only)
FORCE=false
# Filter by tags (comma-separated, e.g. sports,news)
TAGS=
# Cron schedule (minute hour day month weekday)
# Examples: 0 3 * * * (daily 3am), 0 */6 * * * (every 6hrs)
CRON_SCHEDULE=0 3 * * *
# Run once and exit (ignores cron)
RUN_ONCE=false
# Timezone (e.g. America/New_York, Europe/London)
TZ=UTC
# --- Example Configurations ---
# Test: DRY_RUN=true FIRST_ONLY=true
# Production: DRY_RUN=false MODE=copy
# By tags: TAGS=sports,news DRY_RUN=false
# Run now: RUN_ONCE=true DRY_RUN=false

View File

@@ -0,0 +1,32 @@
name: Build & Push Docker Image
on: push
jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Output Repository Information
run: |
echo This workflow will build and push a Docker image to:
echo ${{ vars.REPO_ADDRESS }}/${{ vars.REPO_USER }}/${{ vars.REPO_NAME }}:latest
echo https://${{ vars.REPO_ADDRESS }}/${{ vars.REPO_USER }}/${{ vars.REPO_NAME }}
- name: Build Docker Image
run: |
docker build -t ${{ vars.REPO_ADDRESS }}/${{ vars.REPO_USER }}/${{ vars.REPO_NAME }}:latest --output type=docker --platform linux/amd64 .
- name: Log in to Container Repo
uses: docker/login-action@v3
with:
username: ${{ vars.REPO_USER }}
registry: ${{ vars.REPO_ADDRESS }}
password: ${{ secrets.REPO_TOKEN }}
- name: Push Docker Image
run: |
echo "Pushing Docker image with tag: latest to ${{ vars.REPO_ADDRESS }}"
docker push ${{ vars.REPO_ADDRESS }}/${{ vars.REPO_USER }}/${{ vars.REPO_NAME }}:latest
docker rmi ${{ vars.REPO_ADDRESS }}/${{ vars.REPO_USER }}/${{ vars.REPO_NAME }}:latest || true

24
Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
FROM python:3.11-slim
# Install cron and timezone handling
RUN apt-get update && \
apt-get install -y cron tzdata && \
rm -rf /var/lib/apt/lists/*
# Create app directory
WORKDIR /app
# Copy the Python script
COPY update_channel_logos.py /app/
# Copy the entrypoint script
COPY entrypoint.sh /app/
# Make the entrypoint executable
RUN chmod +x /app/entrypoint.sh
# Set timezone (can be overridden)
ENV TZ=UTC
# Run the entrypoint
ENTRYPOINT ["/app/entrypoint.sh"]

194
README.md
View File

@@ -7,9 +7,42 @@ Automatically copy or clear Live TV channel logos in Emby Server.
- **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
## Quick Start (Docker - Recommended)
1. **Copy the environment template:**
```bash
cp .env.example .env
```
2. **Edit `.env` and set your Emby credentials:**
```bash
EMBY_SERVER_URL=https://your-emby-server.com
EMBY_API_KEY=your_api_key_here
```
3. **Test with dry run on first channel:**
```bash
docker compose up
```
4. **When ready, enable execution:**
Edit `.env` and set:
```bash
DRY_RUN=false
FIRST_ONLY=false
```
5. **Run in the background:**
```bash
docker compose up -d
```
See the [Docker Usage](#docker-usage-recommended) section for detailed configuration.
## Requirements
- Python 3.6+
- **Docker** (recommended): Docker and Docker Compose - uses pre-built image from GHCR
- **Python** (alternative): Python 3.6+
- Emby Server with API access
- API key from Emby Server
@@ -19,7 +52,150 @@ Automatically copy or clear Live TV channel logos in Emby Server.
2. Go to Settings → Advanced → API Keys
3. Create a new API key or use an existing one
## Usage
## Docker Usage (Recommended)
### Configuration
All configuration is done via the `.env` file. Copy `.env.example` to `.env` and customize:
```bash
cp .env.example .env
```
#### Required Settings
| Variable | Description |
|----------|-------------|
| `EMBY_SERVER_URL` | Your Emby server URL (e.g., `https://emby.example.com`) |
| `EMBY_API_KEY` | Your Emby API key |
#### Optional Settings
| Variable | Default | Description |
|----------|---------|-------------|
| `MODE` | `copy` | Operation mode: `copy` (copy logos) or `clear` (delete logos) |
| `DRY_RUN` | `true` | `true` = simulate only, `false` = execute changes |
| `FIRST_ONLY` | `false` | `true` = process first channel only (for testing) |
| `FORCE` | `false` | `true` = overwrite existing logos (copy mode only) |
| `TAGS` | _(empty)_ | Comma-separated list of tags to filter channels (e.g., `sports,news`) |
| `CRON_SCHEDULE` | `0 3 * * *` | Cron schedule (default: daily at 3 AM) |
| `RUN_ONCE` | `false` | `true` = run once and exit, `false` = run on schedule |
| `TZ` | `UTC` | Timezone for cron (e.g., `America/New_York`) |
### Common Scenarios
#### Safe Testing (Recommended First Run)
```bash
# In .env:
MODE=copy
DRY_RUN=true
FIRST_ONLY=true
# Run:
docker compose up
```
#### Production - Daily Updates
```bash
# In .env:
MODE=copy
DRY_RUN=false
CRON_SCHEDULE=0 3 * * *
# Run in background:
docker compose up -d
```
#### One-Time Update Now
```bash
# In .env:
MODE=copy
DRY_RUN=false
RUN_ONCE=true
# Run:
docker compose up
```
#### Force Overwrite Existing Logos
```bash
# In .env:
MODE=copy
DRY_RUN=false
FORCE=true
# Run:
docker compose up -d
```
#### Clear All Logos (Use with Caution!)
```bash
# In .env:
MODE=clear
DRY_RUN=false
# Run:
docker compose up
```
#### Process Only Channels with Specific Tags
```bash
# In .env:
MODE=copy
DRY_RUN=false
TAGS=sports,news
# Run:
docker compose up -d
```
#### Force Update Sports Channels Only
```bash
# In .env:
MODE=copy
DRY_RUN=false
FORCE=true
TAGS=sports
# Run:
docker compose up
```
### Docker Commands
```bash
# Build and start
docker compose up
# Start in background
docker compose up -d
# View logs
docker compose logs -f
# Stop
docker compose down
# Build locally (if modifying source)
# Uncomment "build: ." in docker-compose.yml, then:
docker compose up --build
```
### Cron Schedule Examples
Format: `minute hour day month weekday`
| Schedule | Description |
|----------|-------------|
| `0 3 * * *` | Daily at 3 AM (default) |
| `0 */6 * * *` | Every 6 hours |
| `0 0 * * 0` | Weekly on Sunday at midnight |
| `0 2 1 * *` | Monthly on the 1st at 2 AM |
| `*/30 * * * *` | Every 30 minutes |
## Python Usage (Alternative)
If you prefer to run the script directly with Python instead of Docker:
### Copy Logos (Default Mode)
@@ -76,6 +252,8 @@ python update_channel_logos.py \
| `--first-only` | Only process the first channel (useful for testing) |
| `--clear` | Clear all logos instead of copying them |
| `--force` | Overwrite existing LogoLight/LogoLightColor (copy mode only) |
| `--tags` | Comma-separated list of tags to filter channels (e.g., `sports,news`) |
| `--non-interactive` | Skip confirmation prompts (for automated execution) |
## Examples
@@ -96,15 +274,21 @@ python update_channel_logos.py \
python update_channel_logos.py --server URL --api-key KEY --execute
```
4. Process only channels with specific tags:
```bash
python update_channel_logos.py --server URL --api-key KEY --tags sports,news --execute
```
## How It Works
1. Connects to your Emby server
2. Fetches all Live TV channels
3. For each channel:
3. Filters channels by tags (if specified)
4. 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, unless `--force` is used) or have no logos (clear mode)
5. Shows a summary of results
5. Skips channels that already have the logos (copy mode, unless `--force` is used) or have no logos (clear mode)
6. Shows a summary of results
## Safety Features

19
docker-compose.yml Normal file
View File

@@ -0,0 +1,19 @@
version: '3.8'
services:
emby-logo-tools:
image: ghcr.io/sethwv/emby-logo-tools:latest
# build: . # Uncomment to build locally instead
container_name: emby-logo-tools
restart: unless-stopped
environment:
- EMBY_SERVER_URL=${EMBY_SERVER_URL}
- EMBY_API_KEY=${EMBY_API_KEY}
- MODE=${MODE:-copy}
- DRY_RUN=${DRY_RUN:-true}
- FIRST_ONLY=${FIRST_ONLY:-false}
- FORCE=${FORCE:-false}
- TAGS=${TAGS:-}
- CRON_SCHEDULE=${CRON_SCHEDULE:-0 3 * * *}
- RUN_ONCE=${RUN_ONCE:-false}
- TZ=${TZ:-UTC}

135
entrypoint.sh Normal file
View File

@@ -0,0 +1,135 @@
#!/bin/bash
set -e
# Function to build the command based on environment variables
build_command() {
local cmd="python3 /app/update_channel_logos.py"
# Required parameters
cmd="$cmd --server \"$EMBY_SERVER_URL\""
cmd="$cmd --api-key \"$EMBY_API_KEY\""
# Optional parameters
if [ "${MODE:-copy}" = "clear" ]; then
cmd="$cmd --clear"
fi
if [ "${DRY_RUN:-true}" != "true" ]; then
cmd="$cmd --execute"
fi
if [ "${FIRST_ONLY:-false}" = "true" ]; then
cmd="$cmd --first-only"
fi
if [ "${FORCE:-false}" = "true" ] && [ "${MODE:-copy}" = "copy" ]; then
cmd="$cmd --force"
fi
# Add tags if specified
if [ -n "${TAGS:-}" ]; then
cmd="$cmd --tags \"$TAGS\""
fi
# Add non-interactive flag
cmd="$cmd --non-interactive"
echo "$cmd"
}
# Validate required environment variables
if [ -z "$EMBY_SERVER_URL" ]; then
echo "ERROR: EMBY_SERVER_URL environment variable is required"
exit 1
fi
if [ -z "$EMBY_API_KEY" ]; then
echo "ERROR: EMBY_API_KEY environment variable is required"
exit 1
fi
# Get the cron schedule (default: daily at 3 AM)
CRON_SCHEDULE="${CRON_SCHEDULE:-0 3 * * *}"
# Build the command
COMMAND=$(build_command)
# Log the configuration
echo "=========================================="
echo "Emby TV Logo Tools - Docker Container"
echo "=========================================="
echo "Emby Server: $EMBY_SERVER_URL"
echo "Mode: ${MODE:-copy}"
echo "Dry Run: ${DRY_RUN:-true}"
echo "First Only: ${FIRST_ONLY:-false}"
echo "Force: ${FORCE:-false}"
echo "Tags: ${TAGS:-(none)}"
echo "Cron Schedule: $CRON_SCHEDULE"
echo "Timezone: ${TZ:-UTC}"
echo "=========================================="
echo ""
# If RUN_ONCE is set, just run the command once and exit
if [ "${RUN_ONCE:-false}" = "true" ]; then
echo "Running once (RUN_ONCE=true)..."
eval "$COMMAND"
exit 0
fi
# Create cron job
echo "Setting up cron job..."
echo "$CRON_SCHEDULE $COMMAND >> /var/log/cron.log 2>&1" > /etc/cron.d/emby-logo-update
# Give execution rights on the cron job
chmod 0644 /etc/cron.d/emby-logo-update
# Create the log file
touch /var/log/cron.log
# Apply cron job
crontab /etc/cron.d/emby-logo-update
# Print next scheduled run times
echo ""
echo "Cron job installed. Next 5 scheduled runs:"
echo "=========================================="
# Calculate next run times (this is approximate)
python3 - <<EOF
from datetime import datetime, timedelta
import sys
schedule = "${CRON_SCHEDULE}"
parts = schedule.split()
if len(parts) != 5:
print("Invalid cron schedule format")
sys.exit(1)
minute, hour = parts[0], parts[1]
# Simple calculation for daily cron jobs
if parts[2] == '*' and parts[3] == '*' and parts[4] == '*':
now = datetime.now()
target_hour = int(hour) if hour != '*' else now.hour
target_minute = int(minute) if minute != '*' else 0
next_run = now.replace(hour=target_hour, minute=target_minute, second=0, microsecond=0)
if next_run <= now:
next_run += timedelta(days=1)
for i in range(5):
print(next_run.strftime("%Y-%m-%d %H:%M:%S"))
next_run += timedelta(days=1)
else:
print("(Schedule calculation only supports simple daily jobs)")
print("Your schedule: $CRON_SCHEDULE")
EOF
echo "=========================================="
echo ""
echo "Starting cron daemon..."
echo "Container is running. Logs will appear below."
echo ""
# Start cron in foreground and tail the log
cron && tail -f /var/log/cron.log

View File

@@ -92,6 +92,20 @@ class EmbyClient:
raise Exception(f"Failed to delete {image_type}: HTTP {e.code} - {error_body}")
def filter_channels_by_tags(channels: List[Dict], tags: Optional[List[str]]) -> List[Dict]:
"""Filter channels by tags. Returns all channels if tags is None or empty."""
if not tags:
return channels
filtered = []
for channel in channels:
channel_tags = channel.get('Tags', [])
if any(tag in channel_tags for tag in tags):
filtered.append(channel)
return filtered
def copy_primary_to_light_logos(client: EmbyClient, channel_id: str, channel_name: str, dry_run: bool = True, force: bool = False) -> bool:
try:
channel_data = client.get_channel_by_id(channel_id)
@@ -172,8 +186,17 @@ def clear_channel_logos(client: EmbyClient, channel_id: str, channel_name: str,
return False
def update_channel_logos(client: EmbyClient, dry_run: bool = True, first_only: bool = False, force: bool = False) -> None:
def update_channel_logos(client: EmbyClient, dry_run: bool = True, first_only: bool = False, force: bool = False, tags: Optional[List[str]] = None) -> None:
channels = client.get_live_tv_channels()
# Filter by tags if specified
if tags:
print(f"Filtering channels by tags: {', '.join(tags)}")
channels = filter_channels_by_tags(channels, tags)
if not channels:
print(f"\n⚠️ No channels found with the specified tags")
return
channels_to_process = [channels[0]] if first_only else channels
print("\n" + "=" * 80)
@@ -208,8 +231,17 @@ def update_channel_logos(client: EmbyClient, dry_run: bool = True, first_only: b
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:
def clear_logos(client: EmbyClient, dry_run: bool = True, first_only: bool = False, tags: Optional[List[str]] = None) -> None:
channels = client.get_live_tv_channels()
# Filter by tags if specified
if tags:
print(f"Filtering channels by tags: {', '.join(tags)}")
channels = filter_channels_by_tags(channels, tags)
if not channels:
print(f"\n⚠️ No channels found with the specified tags")
return
channels_to_process = [channels[0]] if first_only else channels
print("\n" + "=" * 80)
@@ -250,7 +282,17 @@ def main():
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)')
parser.add_argument('--force', action='store_true', help='Overwrite existing logos (only applies to copy mode)')
parser.add_argument('--non-interactive', action='store_true', help='Skip confirmation prompts (for automated execution)')
parser.add_argument('--tags', help='Comma-separated list of tags to filter channels (e.g., "sports,news")')
args = parser.parse_args()
# Parse tags if provided
tags = None
if args.tags:
tags = [tag.strip() for tag in args.tags.split(',') if tag.strip()]
if not tags:
print("Warning: --tags specified but no valid tags found", file=sys.stderr)
tags = None
client = EmbyClient(args.server, args.api_key)
@@ -265,22 +307,24 @@ def main():
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)
tag_info = f" with tags [{', '.join(tags)}]" if tags else ""
if args.non_interactive or input(f"\nCLEAR ALL LOGOS from {target}{tag_info}? (yes/no): ").lower() == 'yes':
clear_logos(client, dry_run=False, first_only=args.first_only, tags=tags)
else:
print("Cancelled.")
else:
clear_logos(client, dry_run=True, first_only=args.first_only)
clear_logos(client, dry_run=True, first_only=args.first_only, tags=tags)
else:
if args.execute:
target = "first channel" if args.first_only else "all channels"
action = "overwrite logos in" if args.force else "modify"
if input(f"\n{action.capitalize()} {target}? (yes/no): ").lower() == 'yes':
update_channel_logos(client, dry_run=False, first_only=args.first_only, force=args.force)
tag_info = f" with tags [{', '.join(tags)}]" if tags else ""
if args.non_interactive or input(f"\n{action.capitalize()} {target}{tag_info}? (yes/no): ").lower() == 'yes':
update_channel_logos(client, dry_run=False, first_only=args.first_only, force=args.force, tags=tags)
else:
print("Cancelled.")
else:
update_channel_logos(client, dry_run=True, first_only=args.first_only, force=args.force)
update_channel_logos(client, dry_run=True, first_only=args.first_only, force=args.force, tags=tags)
if __name__ == '__main__':