### Load YouTube API Key - Python Source: https://context7.com/zeroqi/youtube-agent.bundle/llms.txt Loads the YouTube API key from a local 'youtube-key.txt' file if it exists, otherwise falls back to the Plex plugin's preferences. Ensures the API key is loaded correctly for agent functionality. ```python import os # Assuming PluginDir and Data are pre-defined # from pms.plugin import PluginDir # from pms.core.data import Data # from pms.core.log import Log # from pms.core.prefs import Prefs def youtube_api_key(): path = os.path.join(PluginDir, "youtube-key.txt") if os.path.isfile(path): value = Data.Load(path) if value: value = value.strip() if value: Log.Debug("Loaded token from youtube-token.txt file") return value # Fall back to Library preference return Prefs['YouTube-Agent_youtube_api_key'] ``` -------------------------------- ### YouTube Agent Plugin Preferences Configuration in JSON Source: https://context7.com/zeroqi/youtube-agent.bundle/llms.txt This JSON array defines default preferences for the YouTube Agent plugin, including options for metadata handling like setting usernames as directors, using crowd-sourced titles, selecting poster sources, and providing a YouTube API key. Purpose is to allow user customization via Plex's settings interface without altering code; dependencies are Plex's preference system; inputs are user selections; outputs are applied booleans, enums, or text values; limitations include the default API key being a placeholder that users must replace with their own. ```json [ { "id": "add_user_as_director", "label": "Set YouTube usernames as director in metadata", "type": "bool", "default": "false" }, { "id": "use_crowd_sourced_titles", "label": "Use Crowd Sourced Video Titles from DeArrow", "type": "bool", "default": "false" }, { "id": "media_poster_source", "label": "Media Poster", "type": "enum", "default": "Channel", "values": ["Channel", "Episode"] }, { "id": "YouTube-Agent_youtube_api_key", "label": "YouTube API Key (put your own key)", "type": "text", "default": "AIzaSyC2q8yjciNdlYRNdvwbb7NEcDxBkv1Cass" } ] ``` -------------------------------- ### Populate TV Series Episode Metadata - Python Source: https://context7.com/zeroqi/youtube-agent.bundle/llms.txt Provides a placeholder for populating episode metadata for TV series within the Plex agent. This function is intended to handle metadata derived from playlist items and supports various organization methods, acting as a central point for episode data. ```python # Populate episode metadata for TV series from playlist items # (Function implementation details would follow here) ``` -------------------------------- ### Match YouTube Episodes to Local Media and Update Metadata in Python Source: https://context7.com/zeroqi/youtube-agent.bundle/llms.txt This code iterates through seasons and episodes of media, matching video IDs from filenames to YouTube playlist or channel items to update metadata such as title, summary, release date, thumbnail, and rating. It uses Plex's Dict helper for safe dictionary access and falls back to local .info.json files if no API match is found, parsing details like duration and like/dislike counts for rating calculation. Dependencies include Plex framework (e.g., Datetime, Proxy), os, and JSON modules; inputs are media objects and JSON data; outputs are enriched episode metadata; limitations include reliance on filename substring matching and availability of thumbnail URLs. ```python for s in sorted(media.seasons, key=natural_sort_key): for e in sorted(media.seasons[s].episodes, key=natural_sort_key): filename = os.path.basename(media.seasons[s].episodes[e].items[0].parts[0].file) episode = metadata.seasons[s].episodes[e] # Try to match video ID with playlist/channel items for video in Dict(json_playlist_items, 'items') or Dict(json_channel_items, 'items') or {}: videoId = Dict(video, 'id', 'videoId') or Dict(video, 'snippet', 'resourceId', 'videoId') if videoId and videoId in filename: episode.title = sanitize_path(Dict(video, 'snippet', 'title')) episode.summary = sanitize_path(Dict(video, 'snippet', 'description')) episode.originally_available_at = Datetime.ParseDate( Dict(video, 'contentDetails', 'videoPublishedAt') or Dict(video, 'snippet', 'publishedAt') ).date() # Add episode thumbnail thumb = Dict(video, 'snippet', 'thumbnails', 'maxres', 'url') or \ Dict(video, 'snippet', 'thumbnails', 'medium', 'url') or \ Dict(video, 'snippet', 'thumbnails', 'standard', 'url') or \ Dict(video, 'snippet', 'thumbnails', 'high', 'url') or \ Dict(video, 'snippet', 'thumbnails', 'default', 'url') if thumb and thumb not in episode.thumbs: episode.thumbs[thumb] = Proxy.Media(HTTP.Request(thumb).content, sort_order=1) break else: # Fallback: Load from local .info.json file json_filename = filename.rsplit('.', 1)[0] + ".info.json" for root, dirnames, filenames in os.walk(series_root_folder): if json_filename in filenames: json_file = os.path.join(root, json_filename) json_video_details = JSON.ObjectFromString(Core.storage.load(json_file)) videoId = Dict(json_video_details, 'id') episode.title = sanitize_path(Dict(json_video_details, 'title')) episode.summary = sanitize_path(Dict(json_video_details, 'description')) episode.originally_available_at = Datetime.ParseDate( Dict(json_video_details, 'upload_date') ).date() episode.duration = int(Dict(json_video_details, 'duration')) # Calculate rating if int(Dict(json_video_details, 'like_count')) > 0: episode.rating = float(10 * int(Dict(json_video_details, 'like_count')) / (int(Dict(json_video_details, 'dislike_count')) + int(Dict(json_video_details, 'like_count')))) break ``` -------------------------------- ### Plex Agent Classes for YouTube Movies and Series in Python Source: https://context7.com/zeroqi/youtube-agent.bundle/llms.txt This code defines two Plex agent classes: YouTubeSeriesAgent for TV Shows and YouTubeMovieAgent for Movies, both inheriting from Plex's Agent framework to handle local media integration with YouTube. They implement search and update methods calling external Search and Update functions, specifying acceptance from local media agents and no language restrictions. Purpose is to enable metadata scraping for YouTube content in respective libraries; dependencies include Plex's Agent, Locale, and custom Search/Update modules; inputs are results, media, language, and manual/force flags; outputs are populated search results or updated metadata; limitations include primary provider status and no fallback agents. ```python # YouTubeSeries Agent for TV Shows library class YouTubeSeriesAgent(Agent.TV_Shows): name = 'YouTubeSeries' primary_provider = True fallback_agent = None contributes_to = None accepts_from = ['com.plexapp.agents.localmedia'] languages = [Locale.Language.NoLanguage] def search(self, results, media, lang, manual): Search(results, media, lang, manual, False) def update(self, metadata, media, lang, force): Update(metadata, media, lang, force, False) # YouTubeMovie Agent for Movies library class YouTubeMovieAgent(Agent.Movies): name = 'YouTubeMovie' primary_provider = True fallback_agent = None contributes_to = None accepts_from = ['com.plexapp.agents.localmedia'] languages = [Locale.Language.NoLanguage] def search(self, results, media, lang, manual): Search(results, media, lang, manual, True) def update(self, metadata, media, lang, force): Update(metadata, media, lang, force, True) # Enable in Plex: Library Settings > Advanced > Agent ``` -------------------------------- ### Paginated YouTube API Data Loader - Python Source: https://context7.com/zeroqi/youtube-agent.bundle/llms.txt The json_load function fetches paginated YouTube API data by iterating through pages up to 50 times, handling errors and extending results into a single JSON object. It uses URL templates with API keys for endpoints like videos or playlists, requiring JSON, Dict, and HTTP modules; inputs are a URL template and arguments, outputs a merged JSON dict. Limitations include a max iteration cap to prevent infinite loops and basic error raising for API issues. ```python # Fetch YouTube playlist items with automatic pagination # Example: Load all videos from a playlist with 150+ videos def json_load(template, *args): url = template.format(*args + tuple([youtube_api_key()])) url = sanitize_path(url) iteration = 0 json_page = {} json = {} # Iterate through paginated results while not json or Dict(json_page, 'nextPageToken') and iteration < 50: try: json_page = JSON.ObjectFromURL( url + '&pageToken=' + Dict(json_page, 'nextPageToken') if Dict(json_page, 'nextPageToken') else url ) except Exception as e: json = JSON.ObjectFromString(e.content) raise ValueError('code: {}, message: {}'.format( Dict(json, 'error', 'code'), Dict(json, 'error', 'message') )) if json: json['items'].extend(json_page['items']) else: json = json_page iteration += 1 return json # Example API endpoint templates YOUTUBE_API_BASE_URL = "https://www.googleapis.com/youtube/v3/" YOUTUBE_PLAYLIST_ITEMS = YOUTUBE_API_BASE_URL + 'playlistItems?part=snippet,contentDetails&maxResults=50&playlistId={}&key={}' YOUTUBE_json_video_details = YOUTUBE_API_BASE_URL + 'videos?part=snippet,contentDetails,statistics&id={}&key={}' # Usage example playlist_id = "PL22J3VaeABQD_IZs7y60I3lUrrFTzkpat" playlist_items = json_load(YOUTUBE_PLAYLIST_ITEMS, playlist_id) for item in playlist_items['items']: video_id = item['snippet']['resourceId']['videoId'] print("Video: {} - {}".format(video_id, item['snippet']['title'])) ``` -------------------------------- ### Update Movie Metadata from JSON (Python) Source: https://context7.com/zeroqi/youtube-agent.bundle/llms.txt This snippet demonstrates updating metadata for a movie by loading details from a local .info.json file. It parses the JSON to extract video information, including the upload date for the year. ```python # Update metadata for a movie from local .info.json file ``` -------------------------------- ### Extract YouTube IDs and Match Metadata (Python) Source: https://context7.com/zeroqi/youtube-agent.bundle/llms.txt The Search function extracts YouTube IDs from filenames or .info.json files to match media with Plex's metadata system. It supports movie and TV series libraries and handles various ID patterns. ```python # Search for a movie file with YouTube ID in filename # Example filename: "Person Of Interest Soundtrack - John Reese Themes [OR5EnqdnwK0].mp4" def Search(results, media, lang, manual, movie): displayname = sanitize_path(os.path.basename((media.name if movie else media.show) or "")) filename = media.items[0].parts[0].file if movie else media.filename or media.show dir = GetMediaDir(media, movie) # Try to match YouTube video ID pattern [xxxxxxxx] for regex, url in [('VIDEO', YOUTUBE_VIDEO_REGEX)]: result = url.search(filename) if result: guid = result.group('id') results.Append(MetadataSearchResult( id='youtube|{}|{}'.format(guid, os.path.basename(dir)), name=displayname, year=None, score=100, lang=lang )) return # Fallback: Try loading local .info.json file json_filename = os.path.join(dir, os.path.splitext(filename)[0] + ".info.json") if os.path.exists(json_filename): json_video_details = JSON.ObjectFromString(Core.storage.load(json_filename)) video_id = Dict(json_video_details, 'id') results.Append(MetadataSearchResult( id='youtube|{}|{}'.format(video_id, os.path.basename(dir)), name=displayname, year=Datetime.ParseDate(Dict(json_video_details, 'upload_date')).year, score=100, lang=lang )) # Regex patterns used for ID extraction YOUTUBE_VIDEO_REGEX = Regex('(?:^\d{8}_|\[(?:youtube\-)?)(?P[a-z0-9\-_]{11})(?:\]|_)', Regex.IGNORECASE) YOUTUBE_PLAYLIST_REGEX = Regex('\[(?:youtube(|3)\-)?(?PPL[^[]]{16}|PL[^[]]{32}|UU[^[]]{22}|FL[^[]]{22}|LP[^[]]{22}|RD[^[]]{22}|UC[^[]]{22}|HC[^[]]{22})\]', Regex.IGNORECASE) YOUTUBE_CHANNEL_REGEX = Regex('\[(?:youtube(|2)\-)?(?PUC[a-zA-Z0-9\-_]{22}|HC[a-zA-Z0-9\-_]{22})\]') ``` -------------------------------- ### Convert ISO8601 Duration to Seconds - Python Source: https://context7.com/zeroqi/youtube-agent.bundle/llms.txt Converts YouTube's ISO 8601 duration format string (e.g., 'PT1H30M45S') into the total number of seconds. This is crucial for setting accurate duration metadata in Plex. ```python import re # Helper function to safely convert potential number strings to int def js_int(i): return int(''.join([x for x in list(i or '0') if x.isdigit()])) # Convert ISO8601 duration to seconds # Example: "PT1H30M45S" -> 5445 seconds def ISO8601DurationToSeconds(duration): try: # Regex to capture hours, minutes, and seconds parts match = re.match('PT(\\d+H)?(\\d+M)?(\\d+S)?', duration).groups() except: # Return 0 if the duration string is invalid or doesn't match the pattern return 0 else: # Calculate total seconds from captured groups return 3600 * js_int(match[0]) + 60 * js_int(match[1]) + js_int(match[2]) # Usage examples duration1 = "PT1H30M45S" # 1 hour, 30 minutes, 45 seconds seconds1 = ISO8601DurationToSeconds(duration1) # Returns: 5445 duration2 = "PT5M30S" # 5 minutes, 30 seconds seconds2 = ISO8601DurationToSeconds(duration2) # Returns: 330 duration3 = "PT2H15M" # 2 hours, 15 minutes seconds3 = ISO8601DurationToSeconds(duration3) # Returns: 8100 # Convert to milliseconds for Plex metadata # metadata.duration = ISO8601DurationToSeconds(duration) * 1000 ``` -------------------------------- ### Update Video Metadata from Local JSON or YouTube API - Python Source: https://context7.com/zeroqi/youtube-agent.bundle/llms.txt The Update function populates Plex media metadata for YouTube videos, first attempting to load details from a local .info.json file generated from the video title. If unavailable, it falls back to the YouTube API using the video ID. It handles titles, summaries, durations, genres, dates, thumbnails, ratings from likes/dislikes, and optionally adds the uploader as director; requires Plex API objects, os, JSON, and custom helpers like get_thumb and sanitize_path. ```python def Update(metadata, media, lang, force, movie): temp1, guid, series_folder = metadata.id.split("|") dir = sanitize_path(GetMediaDir(media, movie)) if movie: # Try to load metadata from local .info.json file first filename = os.path.basename(media.items[0].parts[0].file) json_filename = os.path.join(dir, os.path.splitext(filename)[0] + ".info.json") if os.path.exists(json_filename): json_video_details = JSON.ObjectFromString(Core.storage.load(json_filename)) guid = Dict(json_video_details, 'id') channel_id = Dict(json_video_details, 'channel_id') # Populate movie metadata metadata.title = Dict(json_video_details, 'title') metadata.summary = Dict(json_video_details, 'description') metadata.duration = Dict(json_video_details, 'duration') metadata.genres = Dict(json_video_details, 'categories') date = Datetime.ParseDate(Dict(json_video_details, 'upload_date')) metadata.originally_available_at = date.date() metadata.year = date.year # Add thumbnail as poster thumb = get_thumb(json_video_details) if thumb and thumb not in metadata.posters: metadata.posters[thumb] = Proxy.Media(HTTP.Request(thumb).content, sort_order=1) # Calculate rating from likes/dislikes if int(Dict(json_video_details, 'like_count')) > 0 and int(Dict(json_video_details, 'dislike_count')) > 0: metadata.rating = float(10 * int(Dict(json_video_details, 'like_count')) / (int(Dict(json_video_details, 'dislike_count')) + int(Dict(json_video_details, 'like_count')))) # Optional: Add uploader as director if Prefs['add_user_as_director']: metadata.directors.clear() director = Dict(json_video_details, 'uploader') meta_director = metadata.directors.new() meta_director.name = director return # Fallback to YouTube API call json_video_details = json_load(YOUTUBE_json_video_details, guid)['items'][0] metadata.title = json_video_details['snippet']['title'] metadata.summary = json_video_details['snippet']['description'] metadata.duration = ISO8601DurationToSeconds(json_video_details['contentDetails']['duration']) * 1000 date = Datetime.ParseDate(json_video_details['snippet']['publishedAt']) metadata.originally_available_at = date.date() metadata.year = date.year ``` -------------------------------- ### Fetch Crowd-Sourced Title from DeArrow API - Python Source: https://context7.com/zeroqi/youtube-agent.bundle/llms.txt Integrates with the DeArrow API to retrieve community-contributed titles for videos. It uses the video ID to generate a hash and query the API, returning a cleaned and title-cased alternative title if available and meeting certain criteria (e.g., positive votes, not locked, not original). ```python import hashlib # Assuming HTTP, JSON, Log, titlecase, and CACHE_1MONTH are pre-defined # from pms.core.http import HTTP # from pms.core.json import JSON # from pms.core.log import Log # from pms.core.prefs import Prefs # from support.string import titlecase def DeArrow(video_id): api_url = 'https://sponsor.ajay.app' hash = hashlib.sha256(video_id.encode('ascii')).hexdigest() # DeArrow API recommends using first 4 hash characters url = '{api_url}/api/branding/{hash}'.format(api_url=api_url, hash=hash[:4]) HTTP.CacheTime = 0 crowd_sourced_title = '' try: data_json = JSON.ObjectFromURL(url) except: Log.Error('DeArrow(): Error while loading JSON.ObjectFromURL. URL: ' + url) return crowd_sourced_title try: first_title_obj = data_json[video_id]['titles'][0] if (first_title_obj['votes'] >= 0 and first_title_obj['locked'] == False and first_title_obj['original'] == False): crowd_sourced_title = titlecase(first_title_obj['title']) except: Log.Info('DeArrow(): No Crowd Sourced Title Found for Video ID: ' + video_id) HTTP.CacheTime = CACHE_1MONTH return crowd_sourced_title # Enable in preferences and use in update function # if Prefs['use_crowd_sourced_titles'] == True: # crowd_sourced_title = DeArrow(guid) # if crowd_sourced_title != '': # metadata.original_title = metadata.title # metadata.summary = 'Original Title: ' + metadata.title + '\r\n\r\n' + metadata.summary # metadata.title = crowd_sourced_title ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.