AnotherMatt 15 Posted August 9, 2025 Posted August 9, 2025 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) 2 1
Recommended Posts
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 accountSign in
Already have an account? Sign in here.
Sign In Now