Jump to content

Simple python script to create a weekly "recently added" newsletter


Recommended Posts

AnotherMatt
Posted

As part of my slow migration process toward Emby, I was missing Tautilli's fantastic 'newsletter' functionality.  So Grok and I worked on a very basic Python proof-of-concept which I though I'd share with the community.  The below script will create an html file containing all media added in the last week.  If you call it without an argument, it will ask you which libraries should be included.  Otherwise you can call it with the '--libraries x,y,z' argument and it'll immediately process.  

It's not pretty, but the basic functionality is there.  I hope it helps anyone else needing this capability.

import requests
import json
from datetime import datetime, timedelta
import getpass
import random
import argparse

# Configuration
EMBY_SERVER = "http://your-emby-server:8096"  # Replace with your Emby server URL
API_KEY = "your-api-key"  # Replace with your Emby API key
USER_ID = "your-user-id"  # Replace with your Emby user ID

# Headers for API requests
headers = {
    "X-Emby-Authorization": f'MediaBrowser Client="Python Script", Device="Script", DeviceId="001", Version="1.0.0"',
    "X-Mediabrowser-Token": API_KEY
}

def get_libraries():
    """Fetch available libraries from Emby."""
    url = f"{EMBY_SERVER}/emby/Library/VirtualFolders?api_key={API_KEY}"
    try:
        response = requests.get(url, headers=headers)
        response.raise_for_status()
        libraries = response.json()
        return [(lib["Name"], lib["ItemId"]) for lib in libraries]  # Note: Use ItemId instead of Id if applicable
    except requests.RequestException as e:
        print(f"Error fetching libraries: {e}")
        return []

def get_user_selection(libraries):
    """Prompt user to select libraries."""
    selection = input("\nEnter the numbers of the libraries to process (e.g., '1,3,4'), or 'all' for all libraries:\n> ").strip().lower()
    
    if selection == "all":
        return [lib_id for _, lib_id in libraries]
    
    try:
        selected_indices = [int(i.strip()) - 1 for i in selection.split(",")]
        return [libraries[i][1] for i in selected_indices if 0 <= i < len(libraries)]
    except (ValueError, IndexError):
        print("Invalid selection. Processing all libraries.")
        return [lib_id for _, lib_id in libraries]

def get_recent_media(library_id, start_date):
    """Fetch media added in the last week for a given library."""
    url = f"{EMBY_SERVER}/emby/Users/{USER_ID}/Items"
    params = {
        "ParentId": library_id,
        "IncludeItemTypes": "Series,Movie,Episode",
        "Recursive": True,
        "Fields": "DateCreated,Overview,SeriesName,ParentIndexNumber,IndexNumber,ImageTags",
        "SortBy": "DateCreated",
        "SortOrder": "Descending",
        "Limit": 1000,  # Set a high limit to fetch many items
        "api_key": API_KEY
    }
    try:
        response = requests.get(url, headers=headers, params=params)
        response.raise_for_status()
        items = response.json().get("Items", [])
        # Filter items where DateCreated >= start_date
        recent_items = [item for item in items if item.get("DateCreated") and datetime.fromisoformat(item["DateCreated"].rstrip('Z')) >= start_date]
        return recent_items
    except requests.RequestException as e:
        print(f"Error fetching media for library {library_id}: {e}")
        return []

def generate_html(media_by_library, selected_library_ids, library_dict):
    """Generate HTML file with media grouped by library, then by series for episodes."""
    # Collect all unique series and movie IDs for random background selection
    all_item_ids = set()
    for lib_id in selected_library_ids:
        media_items = media_by_library.get(lib_id, [])
        series_dict = {}
        movies = []
        for item in media_items:
            if item["Type"] == "Episode" and "SeriesName" in item:
                series_name = item["SeriesName"]
                if series_name not in series_dict:
                    series_dict[series_name] = {
                        "SeriesId": item.get("SeriesId", ""),
                        "Episodes": []
                    }
                series_dict[series_name]["Episodes"].append(item)
            elif item["Type"] == "Movie":
                movies.append(item)
        
        all_item_ids.update([data["SeriesId"] for data in series_dict.values()])
        all_item_ids.update([movie["Id"] for movie in movies])
    
    # Select random backdrop if available
    background_url = ''
    if all_item_ids:
        random_id = random.choice(list(all_item_ids))
        background_url = f"{EMBY_SERVER}/emby/Items/{random_id}/Images/Backdrop?api_key={API_KEY}"
    
    # HTML content with improved professional dark theme, no italics, and background image
    html_content = f"""
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Recently Added Media</title>
        <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
        <style>
            :root {{
                --bg-color: #121212;
                --text-color: #e0e0e0;
                --header-color: #ffffff;
                --card-bg: #1e1e1e;
                --accent-color: #b0b0b0;
                --shadow-color: rgba(0, 0, 0, 0.3);
            }}
            body {{ 
                background-color: var(--bg-color); 
                color: var(--text-color); 
                font-family: 'Roboto', sans-serif; 
                margin: 20px; 
                line-height: 1.6;
                background-image: url('{background_url}');
                background-size: cover;
                background-position: center;
                background-attachment: fixed;
            }}
            .overlay {{
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                background-color: rgba(18, 18, 18, 0.7);
                z-index: -1;
            }}
            h1 {{ 
                text-align: center; 
                color: var(--header-color); 
                font-weight: 700;
                margin-bottom: 40px;
            }}
            h2.library-header {{ 
                grid-column: 1 / -1; 
                color: var(--header-color); 
                font-weight: 500; 
                font-size: 1.8em; 
                margin-bottom: 10px; 
                padding-bottom: 10px; 
                border-bottom: 1px solid var(--accent-color);
            }}
            h3 {{ 
                color: var(--header-color); 
                font-weight: 500; 
                margin-bottom: 10px;
            }}
            .container {{ 
                display: grid; 
                grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 
                gap: 20px; 
                max-width: 1200px; 
                margin: 0 auto;
                position: relative;
                z-index: 1;
            }}
            .card {{ 
                background-color: var(--card-bg); 
                padding: 20px; 
                border-radius: 8px; 
                box-shadow: 0 4px 8px var(--shadow-color); 
                transition: transform 0.2s, box-shadow 0.2s;
            }}
            .card:hover {{
                transform: translateY(-5px);
                box-shadow: 0 6px 12px var(--shadow-color);
            }}
            .card-content {{ 
                display: flex; 
                align-items: flex-start; 
            }}
            img {{ 
                max-width: 120px; 
                height: auto; 
                margin-right: 20px; 
                border-radius: 4px; 
                box-shadow: 0 2px 4px var(--shadow-color);
            }}
            ul {{ 
                list-style-type: disc; 
                padding-left: 20px; 
                margin: 0;
            }}
            li {{ 
                margin-bottom: 8px;
            }}
            .synopsis {{ 
                font-size: 0.9em; 
                color: var(--accent-color); 
            }}
        </style>
    </head>
    <body>
        <div class="overlay"></div>
        <h1>Media Added in the Last Week</h1>
        <div class="container">
    """
    
    for lib_id in selected_library_ids:
        lib_name = library_dict[lib_id]
        media_items = media_by_library.get(lib_id, [])
        if not media_items:
            continue
        
        # Group episodes by series
        series_dict = {}
        movies = []
        for item in media_items:
            if item["Type"] == "Episode" and "SeriesName" in item:
                series_name = item["SeriesName"]
                if series_name not in series_dict:
                    series_dict[series_name] = {
                        "SeriesId": item.get("SeriesId", ""),
                        "Episodes": []
                    }
                series_dict[series_name]["Episodes"].append(item)
            elif item["Type"] == "Movie":
                movies.append(item)
        
        # Sort episodes within each series by season and episode number
        for data in series_dict.values():
            data["Episodes"].sort(key=lambda e: (e.get("ParentIndexNumber", 0), e.get("IndexNumber", 0)))
        
        # Add library header if there is content
        has_content = bool(series_dict or movies)
        if has_content:
            html_content += f'<h2 class="library-header">{lib_name}</h2>'
        
        # Add series cards
        for series_name, data in series_dict.items():
            series_id = data["SeriesId"]
            html_content += f"""
            <div class="card">
                <div class="card-content">
                    <img src="{EMBY_SERVER}/emby/Items/{series_id}/Images/Primary?api_key={API_KEY}" alt="{series_name} poster">
                    <div>
                        <h3>{series_name}</h3>
                        <ul>
            """
            for episode in data["Episodes"]:
                season = episode.get("ParentIndexNumber", "N/A")
                episode_num = episode.get("IndexNumber", "N/A")
                synopsis = episode.get("Overview", "No synopsis available.")
                html_content += f"""
                            <li>S{season:02d}E{episode_num:02d} - {episode['Name']}: <span class="synopsis">{synopsis}</span></li>
                """
            html_content += """
                        </ul>
                    </div>
                </div>
            </div>
            """
        
        # Add movie cards
        for movie in movies:
            movie_id = movie["Id"]
            synopsis = movie.get("Overview", "No synopsis available.")
            html_content += f"""
            <div class="card">
                <div class="card-content">
                    <img src="{EMBY_SERVER}/emby/Items/{movie_id}/Images/Primary?api_key={API_KEY}" alt="{movie['Name']} poster">
                    <div>
                        <h3>{movie['Name']}</h3>
                        <p class="synopsis">{synopsis}</p>
                    </div>
                </div>
            </div>
            """
    
    html_content += """
        </div>
    </body>
    </html>
    """
    
    # Write to file
    with open("recent_media.html", "w", encoding="utf-8") as f:
        f.write(html_content)
    print("HTML file 'recent_media.html' generated successfully.")

def main(args):
    # Calculate date for one week ago
    start_date = datetime.now() - timedelta(days=7)
    
    # Get libraries
    libraries = get_libraries()
    if not libraries:
        print("No libraries found or error occurred.")
        return
    
    library_dict = {lib_id: name for name, lib_id in libraries}
    
    # Print available libraries
    print("\nAvailable Libraries:")
    for i, (name, _) in enumerate(libraries, 1):
        print(f"{i}. {name}")
    
    # Get user selection
    if args.libraries is not None:
        selection = args.libraries.strip().lower()
        if selection == "all":
            selected_library_ids = [lib_id for _, lib_id in libraries]
        else:
            try:
                selected_indices = [int(i.strip()) - 1 for i in selection.split(",")]
                selected_library_ids = [libraries[i][1] for i in selected_indices if 0 <= i < len(libraries)]
            except (ValueError, IndexError):
                print("Invalid command line selection. Falling back to interactive prompt.")
                selected_library_ids = get_user_selection(libraries)
    else:
        selected_library_ids = get_user_selection(libraries)
    
    if not selected_library_ids:
        print("No libraries selected.")
        return
    
    # Fetch recent media for selected libraries
    media_by_library = {}
    has_media = False
    for lib_id in selected_library_ids:
        media_items = get_recent_media(lib_id, start_date)
        media_by_library[lib_id] = media_items
        if media_items:
            has_media = True
    
    if not has_media:
        print("No media found added in the last week.")
        return
    
    # Generate HTML
    generate_html(media_by_library, selected_library_ids, library_dict)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Generate report of recently added media in Emby libraries.")
    parser.add_argument('--libraries', type=str, help="Libraries to process, e.g., '1,3,4' or 'all'")
    args = parser.parse_args()
    
    # Prompt for configuration if not set
    if EMBY_SERVER == "http://your-emby-server:8096":
        EMBY_SERVER = input("Enter your Emby server URL (e.g., http://localhost:8096): ").strip()
    if API_KEY == "your-api-key":
        API_KEY = getpass.getpass("Enter your Emby API key: ").strip()
    if USER_ID == "your-user-id":
        USER_ID = input("Enter your Emby user ID: ").strip()
    
    main(args)

 

  • Like 2
  • Thanks 1

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...