From 52c796f218454d69ee822d51713da5cea7700883 Mon Sep 17 00:00:00 2001
From: MAX <63972751+ltzmax@users.noreply.github.com>
Date: Sun, 4 May 2025 15:28:02 +0200
Subject: [PATCH 01/31] [tmdb] Components v2 support.
---
themoviedb/README.md | 52 ++++++++
themoviedb/info.json | 2 +-
themoviedb/themoviedb.py | 32 +----
themoviedb/tmdb_utils.py | 271 +++++++++++++++++++++++++++++----------
4 files changed, 260 insertions(+), 97 deletions(-)
create mode 100644 themoviedb/README.md
diff --git a/themoviedb/README.md b/themoviedb/README.md
new file mode 100644
index 00000000..24b3bbaf
--- /dev/null
+++ b/themoviedb/README.md
@@ -0,0 +1,52 @@
+Search for informations of movies and TV shows from themoviedb.org.
+
+# [p]tmdbset
+Configure TheMovieDB cog settings.
+ - Usage: `[p]tmdbset`
+ - Restricted to: `BOT_OWNER`
+## [p]tmdbset creds
+Guide to setting up the TMDB API key.
+
+This command will give you information on how to set up the API key.
+ - Usage: `[p]tmdbset creds`
+# [p]movie (Hybrid Command)
+Search for a movie.
+
+You can write the full name of the movie to get more accurate results.
+
+**Examples:**
+- `[p]movie the dark knight`
+- `[p]movie the lord of the rings`
+
+**Arguments:**
+- `` - The movie you want to search for.
+ - Usage: `[p]movie `
+ - Slash Usage: `/movie `
+ - Aliases: `movies`
+# [p]tvshow (Hybrid Command)
+Search for a TV show.
+
+You can write the full name of the TV show to get more accurate results.
+
+**Examples:**
+- `[p]tv the office`
+- `[p]tv game of thrones`
+
+**Arguments:**
+- `` - The TV show you want to search for.
+ - Usage: `[p]tvshow `
+ - Slash Usage: `/tvshow `
+ - Aliases: `tv`
+# [p]person (Hybrid Command)
+Search for a person.
+
+You can write the full name of the person to get more accurate results.
+
+**Examples:**
+- `[p]person arthur`
+- `[p]person johnny depp`
+
+**Arguments:**
+- `` - The person you want to search for.
+ - Usage: `[p]person `
+ - Slash Usage: `/person `
diff --git a/themoviedb/info.json b/themoviedb/info.json
index 31c616a5..22a19054 100644
--- a/themoviedb/info.json
+++ b/themoviedb/info.json
@@ -3,7 +3,7 @@
"max"
],
"name": "TheMovieDB",
- "install_msg": "Thanks for installing.\nYou will need to set your API key before using this cog. See `[p]tmdbset creds`.\n for documentation.",
+ "install_msg": "Thanks for installing.\nYou will need to set your API key before using this cog. See `[p]tmdbset creds`.\n for documentation.\n## PLEASE NOTE:\nThis cog is in alpha phase and using Components V2 and is not fully tested or and stable enough for production use. This also require the Components V2 PR from discord.py for this cog to work.",
"description": "Search for informations of movies and TV shows from themoviedb.org.",
"short": "Search for informations of movies and TV shows from themoviedb.org.",
"hidden": false,
diff --git a/themoviedb/themoviedb.py b/themoviedb/themoviedb.py
index 0c29809a..ab2225a7 100644
--- a/themoviedb/themoviedb.py
+++ b/themoviedb/themoviedb.py
@@ -26,10 +26,11 @@
import aiohttp
import discord
-from redbot.core import Config, app_commands, commands
+from redbot.core import app_commands, commands
from redbot.core.utils.views import SetApiView, SimpleMenu
-from .tmdb_utils import search_and_display, person_embed
+from .tmdb_utils import person_embed, search_and_display
+
class TheMovieDB(commands.Cog):
"""
@@ -37,17 +38,12 @@ class TheMovieDB(commands.Cog):
"""
__author__ = "MAX"
- __version__ = "1.8.0"
- __docs__ = "https://docs.maxapp.tv/docs/tmdb.html"
+ __version__ = "2.0.0a"
+ __docs__ = "https://github.com/ltzmax/maxcogs/tree/master/themoviedb/README.md"
def __init__(self, bot):
self.bot = bot
self.session = aiohttp.ClientSession()
- self.config: Config = Config.get_conf(self, identifier=1234567890, force_registration=True)
- default_global: Dict[str, Union[bool, Optional[int]]] = {
- "use_box": False,
- }
- self.config.register_global(**default_global)
async def cog_unload(self) -> None:
await self.session.close()
@@ -68,19 +64,6 @@ async def tmdbset(self, ctx: commands.Context):
Configure TheMovieDB cog settings.
"""
- @tmdbset.command(name="usebox")
- async def tmdbset_usebox(self, ctx: commands.Context, value: bool):
- """
- Set if you want to use the box in the choose of movie/tv show.
-
- Disabled by default.
-
- - `True` to use the box art in the embeds.
- - `False` to not use the box art in the embeds.
- """
- await self.config.use_box.set(value)
- await ctx.send(f"Use box art set to `{value}`.")
-
@tmdbset.command(name="creds")
@commands.bot_has_permissions(embed_links=True)
async def tmdbset_creds(self, ctx: commands.Context):
@@ -109,7 +92,6 @@ async def tmdbset_creds(self, ctx: commands.Context):
embed.set_footer(text="You can also set your API key by using the button.")
await ctx.send(embed=embed, view=view)
-
@commands.hybrid_command(aliases=["movies"])
@app_commands.describe(query="The movie you want to search for.")
@commands.bot_has_permissions(embed_links=True)
@@ -131,7 +113,7 @@ async def movie(self, ctx: commands.Context, *, query: str):
"The bot owner has not set up the API key for TheMovieDB. "
"Please ask them to set it up."
)
- await search_and_display(ctx, query, "movie", self.config)
+ await search_and_display(ctx, query, "movie")
@commands.hybrid_command(aliases=["tv"])
@app_commands.describe(query="The series you want to search for.")
@@ -154,7 +136,7 @@ async def tvshow(self, ctx: commands.Context, *, query: str):
"The bot owner has not set up the API key for TheMovieDB. "
"Please ask them to set it up."
)
- await search_and_display(ctx, query, "tv", self.config)
+ await search_and_display(ctx, query, "tv")
@commands.hybrid_command()
@app_commands.describe(query="The person you want to search for.")
diff --git a/themoviedb/tmdb_utils.py b/themoviedb/tmdb_utils.py
index d2586a1f..62b4fcd0 100644
--- a/themoviedb/tmdb_utils.py
+++ b/themoviedb/tmdb_utils.py
@@ -26,20 +26,16 @@
import logging
import re
from datetime import datetime
-from typing import Optional, Dict, Any, List
+from typing import Any, Dict, List, Optional
import aiohttp
import discord
import orjson
-from redbot.core.utils.chat_formatting import (
- box,
- header,
- humanize_list,
- humanize_number,
-)
+from redbot.core.bot import Red
+from redbot.core.utils.chat_formatting import box, header, humanize_list, humanize_number
from redbot.core.utils.views import SimpleMenu
-log = logging.getLogger("red.maxcogs.themoviedb.everything_stuff")
+log = logging.getLogger("red.maxcogs.themoviedb.tmdb_utils")
BASE_MEDIA = "https://api.themoviedb.org/3/search"
BASE_URL = "https://api.themoviedb.org/3"
@@ -56,6 +52,7 @@ async def fetch_tmdb(url: str, session: aiohttp.ClientSession) -> Optional[Dict[
log.error(f"TMDB request error: {e}")
return None
+
async def validate_results(ctx, data: Optional[Dict[str, Any]], query: str) -> bool:
"""Validate TMDB response and send appropriate messages."""
if not data:
@@ -67,34 +64,48 @@ async def validate_results(ctx, data: Optional[Dict[str, Any]], query: str) -> b
return False
return True
-def filter_media_results(results: List[Dict[str, Any]], query: str, media_type: str) -> List[Dict[str, Any]]:
+
+def filter_media_results(
+ results: List[Dict[str, Any]], query: str, media_type: str
+) -> List[Dict[str, Any]]:
"""Filter TMDB search results based on query and criteria."""
- # If you remove this, do not ever ask me to help you with this cog.
- # Google is your friend if you do not understand why this is banned.
+ # Banned for reasons of being offensive or not suitable for us norwegians to watch or discuss.
+ # Might add as default config in the future for removal if wanted to or update with more banned titles.
banned_titles = {
- "22 july", "22 july 2011", "utøya: july 22",
- "utoya: july 22", "july 22", "july 22, 2011"
+ "22 july",
+ "22 july 2011",
+ "utøya: july 22",
+ "utoya: july 22",
+ "july 22",
+ "july 22, 2011",
}
key = "title" if media_type == "movie" else "name"
return [
- r for r in results
+ r
+ for r in results
if r.get(key, "").lower().startswith(query.lower())
and r.get("release_date", "N/A")[:4] >= "1799"
and r.get(key, "").lower() not in banned_titles
]
-async def search_media(ctx, session: aiohttp.ClientSession, query: str, media_type: str, page: int = 1) -> Optional[Dict[str, Any]]:
+
+async def search_media(
+ ctx, session: aiohttp.ClientSession, query: str, media_type: str, page: int = 1
+) -> Optional[Dict[str, Any]]:
"""Search for media on TMDB."""
api_key = (await ctx.bot.get_shared_api_tokens("tmdb")).get("api_key")
if not api_key:
log.error("TMDB API key is missing")
await ctx.send("TMDB API key is missing.")
return None
- include_adult = str(getattr(ctx.channel, 'nsfw', False)).lower()
+ include_adult = str(getattr(ctx.channel, "nsfw", False)).lower()
url = f"{BASE_MEDIA}/{media_type}?api_key={api_key}&query={query}&page={page}&include_adult={include_adult}"
return await fetch_tmdb(url, session)
-async def get_media_data(ctx, session: aiohttp.ClientSession, media_id: int, media_type: str) -> Optional[Dict[str, Any]]:
+
+async def get_media_data(
+ ctx, session: aiohttp.ClientSession, media_id: int, media_type: str
+) -> Optional[Dict[str, Any]]:
"""Fetch specific media data from TMDB."""
api_key = (await ctx.bot.get_shared_api_tokens("tmdb")).get("api_key")
if not api_key:
@@ -104,6 +115,7 @@ async def get_media_data(ctx, session: aiohttp.ClientSession, media_id: int, med
url = f"{BASE_URL}/{media_type}/{media_id}?api_key={api_key}"
return await fetch_tmdb(url, session)
+
async def build_embed(ctx, data, item_id, index, results, item_type="movie"):
"""Build a Discord embed for TMDB media data."""
if not data:
@@ -218,8 +230,9 @@ async def build_embed(ctx, data, item_id, index, results, item_type="movie"):
)
return embed, view
-async def search_and_display(ctx, query: str, media_type: str, config):
- """Search TMDB and display results with a selection menu."""
+
+async def search_and_display(ctx, query: str, media_type: str):
+ """Search TMDB and display results with a paginated layout and selection buttons."""
await ctx.typing()
async with aiohttp.ClientSession() as session:
initial_data = await search_media(ctx, session, query, media_type)
@@ -227,7 +240,10 @@ async def search_and_display(ctx, query: str, media_type: str, config):
return
total_pages = min(initial_data.get("total_pages", 1), 20)
- page_tasks = [search_media(ctx, session, query, media_type, page) for page in range(1, total_pages + 1)]
+ page_tasks = [
+ search_media(ctx, session, query, media_type, page)
+ for page in range(1, total_pages + 1)
+ ]
page_results = await asyncio.gather(*page_tasks, return_exceptions=True)
filtered_results = []
@@ -236,62 +252,171 @@ async def search_and_display(ctx, query: str, media_type: str, config):
continue
filtered_results.extend(filter_media_results(data["results"], query, media_type))
- if not filtered_results:
- return await ctx.send(f"No results found for {query}")
+ if not filtered_results:
+ return await ctx.send(f"No results found for `{query}`.")
- if len(filtered_results) == 1:
+ if len(filtered_results) == 1:
+ session = aiohttp.ClientSession()
+ try:
data = await get_media_data(ctx, session, filtered_results[0]["id"], media_type)
if not data:
return await ctx.send("Failed to fetch media details.")
-
- embed, view = await build_embed(ctx, data, filtered_results[0]["id"], 0, filtered_results, item_type=media_type)
- await ctx.send(embed=embed, view=view)
- return
- response_in_box = await config.use_box()
- title = "What would you like to select?"
- header_text = f"{header(title, 'medium')}"
- pages = [
- f"{header_text}\n" + "\n".join(
- f"{i+j+1}. {r['title' if media_type == 'movie' else 'name']} "
- f"({r.get('release_date' if media_type == 'movie' else 'first_air_date', 'N/A')[:4]}) "
- f"({r.get('popularity', 0)})"
- for j, r in enumerate(filtered_results[i:i+15])
+ embed, view = await build_embed(
+ ctx, data, filtered_results[0]["id"], 0, filtered_results, item_type=media_type
+ )
+ await ctx.send(embed=embed, view=view)
+ finally:
+ await session.close()
+ return
+
+ title = "What would you like to select?\n-# Click on the button to views the media details."
+ header_text = f"{header(title, 'medium')}"
+
+ class MediaPaginator(discord.ui.LayoutView):
+ def __init__(self, ctx, filtered_results, media_type):
+ super().__init__(timeout=120)
+ self.ctx = ctx
+ self.filtered_results = filtered_results
+ self.media_type = media_type
+ self.session = aiohttp.ClientSession()
+ self.current_page = 0
+ self.items_per_page = 12
+ self.message = None
+ self._update_content()
+
+ def _update_content(self):
+ self.clear_items()
+ start_idx = self.current_page * self.items_per_page
+ end_idx = min(
+ (self.current_page + 1) * self.items_per_page, len(self.filtered_results)
+ )
+ page_results = self.filtered_results[start_idx:end_idx]
+ self.add_item(discord.ui.TextDisplay(header_text))
+
+ for i, result in enumerate(page_results):
+ label = (
+ f"{start_idx + i + 1}. {result['title' if media_type == 'movie' else 'name']} "
+ f"({result.get('release_date' if media_type == 'movie' else 'first_air_date', 'N/A')[:4]}) "
+ f"({result.get('popularity', 0)})"
+ )
+ section = discord.ui.Section(
+ discord.ui.TextDisplay(label), accessory=MediaButton(start_idx + i)
+ )
+ self.add_item(section)
+
+ if len(self.filtered_results) > self.items_per_page:
+ row = discord.ui.ActionRow()
+ if self.current_page > 0:
+ row.add_item(NavButton("prev"))
+ if end_idx < len(self.filtered_results):
+ row.add_item(NavButton("next"))
+ self.add_item(row)
+
+ async def on_timeout(self):
+ for item in self.children:
+ if isinstance(item, discord.ui.Section) and hasattr(item, "accessory"):
+ if isinstance(item.accessory, discord.ui.Button):
+ item.accessory.disabled = True
+ elif isinstance(item, discord.ui.ActionRow):
+ for child in item.children:
+ if isinstance(child, discord.ui.Button):
+ child.disabled = True
+ if self.message:
+ try:
+ await self.message.edit(content=None, view=self)
+ except discord.NotFound as e:
+ log.error(f"Message not found: {e}", exc_info=True)
+ pass
+ await self.session.close()
+
+ async def stop(self):
+ await self.session.close()
+ super().stop()
+
+ async def interaction_check(self, interaction: discord.Interaction[Red]):
+ if interaction.user != self.ctx.author:
+ await interaction.response.send_message(
+ "You are not the owner of this interaction.", ephemeral=True
+ )
+ return False
+ return True
+
+ class MediaButton(discord.ui.Button["MediaPaginator"]):
+ def __init__(self, index, label=None):
+ super().__init__(label=label or "Select")
+ self.index = index
+
+ async def callback(self, interaction: discord.Interaction[Red]) -> None:
+ try:
+ data = await get_media_data(
+ self.view.ctx,
+ self.view.session,
+ self.view.filtered_results[self.index]["id"],
+ self.view.media_type,
+ )
+ if not data:
+ return await interaction.response.send_message(
+ "Failed to fetch media details.", ephemeral=True
+ )
+ except Exception as e:
+ log.error(f"Error fetching media details: {e}", exc_info=True)
+ return await interaction.response.send_message(
+ "Error fetching media details", ephemeral=True
+ )
+
+ embed, view = await build_embed(
+ self.view.ctx,
+ data,
+ self.view.filtered_results[self.index]["id"],
+ self.index,
+ self.view.filtered_results,
+ item_type=self.view.media_type,
+ )
+ await interaction.response.send_message(embed=embed, view=view)
+
+ for item in self.view.children:
+ if isinstance(item, discord.ui.Section) and hasattr(item, "accessory"):
+ if isinstance(item.accessory, discord.ui.Button):
+ item.accessory.disabled = True
+ elif isinstance(item, discord.ui.ActionRow):
+ for child in item.children:
+ if isinstance(child, discord.ui.Button):
+ child.disabled = True
+ await self.view.message.edit(content=None, view=self.view)
+ await self.view.stop()
+
+ class NavButton(discord.ui.Button["MediaPaginator"]):
+ def __init__(self, direction):
+ super().__init__(
+ label="Previous" if direction == "prev" else "Next",
+ emoji="◀️" if direction == "prev" else "▶️",
+ style=discord.ButtonStyle.secondary,
+ custom_id=direction,
)
- for i in range(0, len(filtered_results), 15)
- ]
-
- try:
- pages = [box(page, lang="prolog") if response_in_box else page for page in pages]
- except ImportError:
- log.warning("box formatting not available, using plain text")
- pass
- menu = SimpleMenu(pages, use_select_menu=True, disable_after_timeout=True, timeout=120)
- await menu.start(ctx)
+ async def callback(self, interaction: discord.Interaction[Red]) -> None:
+ try:
+ start_idx = self.view.current_page * self.view.items_per_page
+ end_idx = start_idx + self.view.items_per_page
+ if self.custom_id == "prev" and self.view.current_page > 0:
+ self.view.current_page -= 1
+ elif self.custom_id == "next" and end_idx < len(self.view.filtered_results):
+ self.view.current_page += 1
+
+ self.view._update_content()
+ await interaction.response.defer()
+ await self.view.message.edit(content=None, view=self.view)
+ except Exception as e:
+ log.error(f"Error in NavButton callback: {str(e)}", exc_info=True)
+ await interaction.response.send_message(
+ f"Error navigating, please try again later.", ephemeral=True
+ )
+
+ paginator = MediaPaginator(ctx, filtered_results, media_type)
+ message = await ctx.send(content="", view=paginator)
+ paginator.message = message
- try:
- msg = await ctx.bot.wait_for(
- "message",
- check=lambda m: m.author == ctx.author and m.channel == ctx.channel,
- timeout=60
- )
- if not msg.content.isdigit():
- return await ctx.send("Invalid input. Exiting.")
-
- index = int(msg.content) - 1
- if index < 0 or index >= len(filtered_results):
- return await ctx.send("Invalid selection. Exiting.")
- except ValueError:
- return await ctx.send("Invalid input. Exiting.")
- except asyncio.TimeoutError:
- return await ctx.send("Selection timed out. Exiting.")
-
- data = await get_media_data(ctx, session, filtered_results[index]["id"], media_type)
- if not data:
- return await ctx.send("Failed to fetch media details.")
- embed, view = await build_embed(ctx, data, filtered_results[index]["id"], index, filtered_results, item_type=media_type)
- await ctx.send(embed=embed, view=view)
async def person_embed(ctx, query: str):
"""Search and display person information from TMDB."""
@@ -301,7 +426,9 @@ async def person_embed(ctx, query: str):
if not await validate_results(ctx, people_data, query):
return
- sorted_people = sorted(people_data["results"], key=lambda x: x.get("popularity", 0), reverse=True)
+ sorted_people = sorted(
+ people_data["results"], key=lambda x: x.get("popularity", 0), reverse=True
+ )
embeds = []
for person in sorted_people:
@@ -313,7 +440,7 @@ async def person_embed(ctx, query: str):
title=data.get("name", "Unknown"),
url=f"https://www.themoviedb.org/person/{person['id']}",
description=data.get("biography", "No biography available.")[:3048],
- colour=await ctx.embed_colour()
+ colour=await ctx.embed_colour(),
)
fields = {
@@ -343,4 +470,6 @@ async def person_embed(ctx, query: str):
if not embeds:
return await ctx.send("No information found for this person.")
- await SimpleMenu(embeds, use_select_menu=True, disable_after_timeout=True, timeout=120).start(ctx)
+ await SimpleMenu(
+ embeds, use_select_menu=True, disable_after_timeout=True, timeout=120
+ ).start(ctx)
From 42ee19cf67d4d6864dc7b83ce505912324b248ac Mon Sep 17 00:00:00 2001
From: MAX <63972751+ltzmax@users.noreply.github.com>
Date: Mon, 5 May 2025 15:37:41 +0200
Subject: [PATCH 02/31] cleanup a little and sort by actual release.
---
themoviedb/tmdb_utils.py | 119 ++++++++++++++++++++++++---------------
1 file changed, 73 insertions(+), 46 deletions(-)
diff --git a/themoviedb/tmdb_utils.py b/themoviedb/tmdb_utils.py
index 62b4fcd0..2ca67736 100644
--- a/themoviedb/tmdb_utils.py
+++ b/themoviedb/tmdb_utils.py
@@ -274,46 +274,73 @@ async def search_and_display(ctx, query: str, media_type: str):
header_text = f"{header(title, 'medium')}"
class MediaPaginator(discord.ui.LayoutView):
- def __init__(self, ctx, filtered_results, media_type):
+ def __init__(self, ctx, filtered_results, media_type, items_per_page=12, session=None):
super().__init__(timeout=120)
self.ctx = ctx
- self.filtered_results = filtered_results
+ self.filtered_results = self._sort_results(filtered_results, media_type)
self.media_type = media_type
- self.session = aiohttp.ClientSession()
+ self.session = session or aiohttp.ClientSession()
+ self.owns_session = session is None
self.current_page = 0
- self.items_per_page = 12
+ self.items_per_page = items_per_page
self.message = None
self._update_content()
- def _update_content(self):
+ def _sort_results(self, results, media_type):
+ """Sort results by release date (descending, newest first)."""
+ date_key = "release_date" if media_type == "movie" else "first_air_date"
+ def get_date(item):
+ date_str = item.get(date_key, "0000-00-00")
+ try:
+ return datetime.strptime(date_str[:10], "%Y-%m-%d").timestamp()
+ except (ValueError, TypeError):
+ return float("-inf")
+ return sorted(results, key=get_date, reverse=True)
+
+ def _get_label(self, result, index):
+ """Generate display label for a media item."""
+ key_map = {
+ "movie": {"title": "title", "date": "release_date"},
+ "tv": {"title": "name", "date": "first_air_date"}
+ }
+ keys = key_map[self.media_type]
+ title = result.get(keys["title"], "Unknown")
+ date = result.get(keys["date"], "N/A")[:4]
+ popularity = result.get("popularity", 0)
+ return f"{index + 1}. {title} ({date}) ({popularity})"
+
+ def _build_page_content(self):
+ """Build content for the current page."""
self.clear_items()
start_idx = self.current_page * self.items_per_page
- end_idx = min(
- (self.current_page + 1) * self.items_per_page, len(self.filtered_results)
- )
+ end_idx = min((self.current_page + 1) * self.items_per_page, len(self.filtered_results))
page_results = self.filtered_results[start_idx:end_idx]
self.add_item(discord.ui.TextDisplay(header_text))
for i, result in enumerate(page_results):
- label = (
- f"{start_idx + i + 1}. {result['title' if media_type == 'movie' else 'name']} "
- f"({result.get('release_date' if media_type == 'movie' else 'first_air_date', 'N/A')[:4]}) "
- f"({result.get('popularity', 0)})"
- )
+ label = self._get_label(result, start_idx + i)
section = discord.ui.Section(
discord.ui.TextDisplay(label), accessory=MediaButton(start_idx + i)
)
self.add_item(section)
+ def _add_navigation_buttons(self):
+ """Add navigation buttons if needed."""
if len(self.filtered_results) > self.items_per_page:
row = discord.ui.ActionRow()
if self.current_page > 0:
row.add_item(NavButton("prev"))
- if end_idx < len(self.filtered_results):
+ if (self.current_page + 1) * self.items_per_page < len(self.filtered_results):
row.add_item(NavButton("next"))
self.add_item(row)
- async def on_timeout(self):
+ def _update_content(self):
+ """Update the paginator's content."""
+ self._build_page_content()
+ self._add_navigation_buttons()
+
+ def _disable_all_buttons(self):
+ """Disable all buttons in the view."""
for item in self.children:
if isinstance(item, discord.ui.Section) and hasattr(item, "accessory"):
if isinstance(item.accessory, discord.ui.Button):
@@ -322,22 +349,26 @@ async def on_timeout(self):
for child in item.children:
if isinstance(child, discord.ui.Button):
child.disabled = True
+
+ async def _cleanup(self):
+ """Clean up resources and update message."""
+ self._disable_all_buttons()
+ if self.owns_session and not self.session.closed:
+ await self.session.close()
if self.message:
try:
await self.message.edit(content=None, view=self)
except discord.NotFound as e:
log.error(f"Message not found: {e}", exc_info=True)
- pass
- await self.session.close()
- async def stop(self):
- await self.session.close()
+ async def on_timeout(self):
+ await self._cleanup()
super().stop()
- async def interaction_check(self, interaction: discord.Interaction[Red]):
+ async def interaction_check(self, interaction: discord.Interaction):
if interaction.user != self.ctx.author:
await interaction.response.send_message(
- "You are not the owner of this interaction.", ephemeral=True
+ f"Only {self.ctx.author.mention} can use this.", ephemeral=True
)
return False
return True
@@ -347,7 +378,13 @@ def __init__(self, index, label=None):
super().__init__(label=label or "Select")
self.index = index
- async def callback(self, interaction: discord.Interaction[Red]) -> None:
+ async def _send_error(self, interaction, message, exc=None):
+ """Send an error message and log if an exception is provided."""
+ if exc:
+ log.error(f"Error fetching media details for ID {self.view.filtered_results[self.index]['id']}: {exc}", exc_info=True)
+ await interaction.response.send_message(message, ephemeral=True)
+
+ async def callback(self, interaction: discord.Interaction) -> None:
try:
data = await get_media_data(
self.view.ctx,
@@ -356,14 +393,11 @@ async def callback(self, interaction: discord.Interaction[Red]) -> None:
self.view.media_type,
)
if not data:
- return await interaction.response.send_message(
- "Failed to fetch media details.", ephemeral=True
- )
+ return await self._send_error(interaction, "Failed to fetch media details.")
+ except aiohttp.ClientConnectionError as e:
+ return await self._send_error(interaction, "Network error, please try again.", e)
except Exception as e:
- log.error(f"Error fetching media details: {e}", exc_info=True)
- return await interaction.response.send_message(
- "Error fetching media details", ephemeral=True
- )
+ return await self._send_error(interaction, "Error fetching media details.", e)
embed, view = await build_embed(
self.view.ctx,
@@ -374,17 +408,7 @@ async def callback(self, interaction: discord.Interaction[Red]) -> None:
item_type=self.view.media_type,
)
await interaction.response.send_message(embed=embed, view=view)
-
- for item in self.view.children:
- if isinstance(item, discord.ui.Section) and hasattr(item, "accessory"):
- if isinstance(item.accessory, discord.ui.Button):
- item.accessory.disabled = True
- elif isinstance(item, discord.ui.ActionRow):
- for child in item.children:
- if isinstance(child, discord.ui.Button):
- child.disabled = True
- await self.view.message.edit(content=None, view=self.view)
- await self.view.stop()
+ await self.view._cleanup()
class NavButton(discord.ui.Button["MediaPaginator"]):
def __init__(self, direction):
@@ -395,7 +419,13 @@ def __init__(self, direction):
custom_id=direction,
)
- async def callback(self, interaction: discord.Interaction[Red]) -> None:
+ async def _send_error(self, interaction, message, exc=None):
+ """Send an error message and log if an exception is provided."""
+ if exc:
+ log.error(f"Error navigating page {self.view.current_page} ({self.custom_id}): {exc}", exc_info=True)
+ await interaction.response.send_message(message, ephemeral=True)
+
+ async def callback(self, interaction: discord.Interaction) -> None:
try:
start_idx = self.view.current_page * self.view.items_per_page
end_idx = start_idx + self.view.items_per_page
@@ -407,11 +437,8 @@ async def callback(self, interaction: discord.Interaction[Red]) -> None:
self.view._update_content()
await interaction.response.defer()
await self.view.message.edit(content=None, view=self.view)
- except Exception as e:
- log.error(f"Error in NavButton callback: {str(e)}", exc_info=True)
- await interaction.response.send_message(
- f"Error navigating, please try again later.", ephemeral=True
- )
+ except Exception:
+ await self._send_error(interaction, "Error navigating, please try again.")
paginator = MediaPaginator(ctx, filtered_results, media_type)
message = await ctx.send(content="", view=paginator)
From b2842484aab4564c0d3273747bf868f5345dfb4c Mon Sep 17 00:00:00 2001
From: MAX <63972751+ltzmax@users.noreply.github.com>
Date: Mon, 5 May 2025 15:38:18 +0200
Subject: [PATCH 03/31] Style.
---
themoviedb/tmdb_utils.py | 18 ++++++++++++++----
1 file changed, 14 insertions(+), 4 deletions(-)
diff --git a/themoviedb/tmdb_utils.py b/themoviedb/tmdb_utils.py
index 2ca67736..493d5806 100644
--- a/themoviedb/tmdb_utils.py
+++ b/themoviedb/tmdb_utils.py
@@ -289,19 +289,21 @@ def __init__(self, ctx, filtered_results, media_type, items_per_page=12, session
def _sort_results(self, results, media_type):
"""Sort results by release date (descending, newest first)."""
date_key = "release_date" if media_type == "movie" else "first_air_date"
+
def get_date(item):
date_str = item.get(date_key, "0000-00-00")
try:
return datetime.strptime(date_str[:10], "%Y-%m-%d").timestamp()
except (ValueError, TypeError):
return float("-inf")
+
return sorted(results, key=get_date, reverse=True)
def _get_label(self, result, index):
"""Generate display label for a media item."""
key_map = {
"movie": {"title": "title", "date": "release_date"},
- "tv": {"title": "name", "date": "first_air_date"}
+ "tv": {"title": "name", "date": "first_air_date"},
}
keys = key_map[self.media_type]
title = result.get(keys["title"], "Unknown")
@@ -313,7 +315,9 @@ def _build_page_content(self):
"""Build content for the current page."""
self.clear_items()
start_idx = self.current_page * self.items_per_page
- end_idx = min((self.current_page + 1) * self.items_per_page, len(self.filtered_results))
+ end_idx = min(
+ (self.current_page + 1) * self.items_per_page, len(self.filtered_results)
+ )
page_results = self.filtered_results[start_idx:end_idx]
self.add_item(discord.ui.TextDisplay(header_text))
@@ -381,7 +385,10 @@ def __init__(self, index, label=None):
async def _send_error(self, interaction, message, exc=None):
"""Send an error message and log if an exception is provided."""
if exc:
- log.error(f"Error fetching media details for ID {self.view.filtered_results[self.index]['id']}: {exc}", exc_info=True)
+ log.error(
+ f"Error fetching media details for ID {self.view.filtered_results[self.index]['id']}: {exc}",
+ exc_info=True,
+ )
await interaction.response.send_message(message, ephemeral=True)
async def callback(self, interaction: discord.Interaction) -> None:
@@ -422,7 +429,10 @@ def __init__(self, direction):
async def _send_error(self, interaction, message, exc=None):
"""Send an error message and log if an exception is provided."""
if exc:
- log.error(f"Error navigating page {self.view.current_page} ({self.custom_id}): {exc}", exc_info=True)
+ log.error(
+ f"Error navigating page {self.view.current_page} ({self.custom_id}): {exc}",
+ exc_info=True,
+ )
await interaction.response.send_message(message, ephemeral=True)
async def callback(self, interaction: discord.Interaction) -> None:
From 3017387bef6d760a40d604201380260f92ad6d92 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 5 May 2025 13:41:34 +0000
Subject: [PATCH 04/31] [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---
autopublisher/autopublisher.py | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/autopublisher/autopublisher.py b/autopublisher/autopublisher.py
index 8d84a66a..d421f535 100644
--- a/autopublisher/autopublisher.py
+++ b/autopublisher/autopublisher.py
@@ -91,7 +91,7 @@ async def _get_owner_timezone(self) -> pytz.timezone:
async def _schedule_resets(self) -> None:
"""Schedule periodic count resets in the owner's timezone."""
owner_tz = await self._get_owner_timezone()
- self.scheduler.remove_all_jobs() # Clear existing jobs to avoid duplicates
+ self.scheduler.remove_all_jobs() # Clear existing jobs to avoid duplicates
self.scheduler.add_job(
self.reset_count,
"cron",
@@ -146,13 +146,17 @@ async def reset_count(self, period: Literal["weekly", "monthly", "yearly"]) -> N
data["published_monthly_count"] = 0
self.logger.info("Monthly count reset.")
else:
- self.logger.debug(f"Skipped monthly reset: not the 1st day (current day: {now_in_owner_tz.day}).")
+ self.logger.debug(
+ f"Skipped monthly reset: not the 1st day (current day: {now_in_owner_tz.day})."
+ )
elif period == "yearly":
if now_in_owner_tz.month == 1 and now_in_owner_tz.day == 1:
data["published_yearly_count"] = 0
self.logger.info("Yearly count reset.")
else:
- self.logger.debug(f"Skipped yearly reset: not Jan 1 (current date: {now_in_owner_tz.month}/{now_in_owner_tz.day}).")
+ self.logger.debug(
+ f"Skipped yearly reset: not Jan 1 (current date: {now_in_owner_tz.month}/{now_in_owner_tz.day})."
+ )
async def increment_published_count(self) -> None:
"""Increment all published message counts."""
From 74fc92d244102b7537f63a4f09ab0bc8128b0616 Mon Sep 17 00:00:00 2001
From: MAX <63972751+ltzmax@users.noreply.github.com>
Date: Thu, 8 May 2025 19:30:54 +0200
Subject: [PATCH 05/31] Add autocomplete.
* Not really good with autocomplete so it may not work properly just yet, i need to maybe rework it and ask someone for comments about it.
---
themoviedb/themoviedb.py | 90 +++++++++++++++++++++++++++++++++++++++-
1 file changed, 88 insertions(+), 2 deletions(-)
diff --git a/themoviedb/themoviedb.py b/themoviedb/themoviedb.py
index ab2225a7..94dfdfc2 100644
--- a/themoviedb/themoviedb.py
+++ b/themoviedb/themoviedb.py
@@ -22,14 +22,16 @@
SOFTWARE.
"""
-from typing import Dict, Optional, Union
+import urllib.parse
+from datetime import datetime
+from typing import Any, Dict, List, Optional, Union
import aiohttp
import discord
from redbot.core import app_commands, commands
from redbot.core.utils.views import SetApiView, SimpleMenu
-from .tmdb_utils import person_embed, search_and_display
+from .tmdb_utils import fetch_tmdb, person_embed, search_and_display
class TheMovieDB(commands.Cog):
@@ -115,6 +117,48 @@ async def movie(self, ctx: commands.Context, *, query: str):
)
await search_and_display(ctx, query, "movie")
+ @movie.autocomplete("query")
+ async def movie_autocomplete(
+ self, interaction: discord.Interaction, current: str
+ ) -> List[app_commands.Choice[str]]:
+ """Autocomplete suggestions for movie search, sorted by release date."""
+ if not current:
+ return []
+
+ token = await self.bot.get_shared_api_tokens("tmdb")
+ api_key = token.get("api_key")
+ if not api_key:
+ return []
+
+ include_adult = str(getattr(interaction.channel, "nsfw", False)).lower()
+ encoded_query = urllib.parse.quote(current)
+ url = f"https://api.themoviedb.org/3/search/movie?api_key={api_key}&query={encoded_query}&page=1&include_adult={include_adult}"
+ async with aiohttp.ClientSession() as session:
+ data = await fetch_tmdb(url, session)
+
+ if not data or "results" not in data:
+ return []
+
+ def get_date(item: Dict[str, Any]) -> float:
+ date_str = item.get("release_date", "")
+ if not date_str or not isinstance(date_str, str) or len(date_str) < 4:
+ return float("-inf")
+ try:
+ year = int(date_str[:4])
+ return datetime(year=year, month=1, day=1).timestamp()
+ except (ValueError, TypeError):
+ return float("-inf")
+
+ sorted_results = sorted(data.get("results", []), key=get_date, reverse=True)
+
+ return [
+ app_commands.Choice(
+ name=f"{result.get('title', 'Unknown')} ({result.get('release_date', '')[:4] or 'N/A'})",
+ value=result.get("title", "Unknown"),
+ )
+ for result in sorted_results[:25]
+ ]
+
@commands.hybrid_command(aliases=["tv"])
@app_commands.describe(query="The series you want to search for.")
@commands.bot_has_permissions(embed_links=True)
@@ -138,6 +182,48 @@ async def tvshow(self, ctx: commands.Context, *, query: str):
)
await search_and_display(ctx, query, "tv")
+ @tvshow.autocomplete("query")
+ async def tvshow_autocomplete(
+ self, interaction: discord.Interaction, current: str
+ ) -> List[app_commands.Choice[str]]:
+ """Autocomplete suggestions for TV show search, sorted by first air date."""
+ if not current:
+ return []
+
+ token = await self.bot.get_shared_api_tokens("tmdb")
+ api_key = token.get("api_key")
+ if not api_key:
+ return []
+
+ include_adult = str(getattr(interaction.channel, "nsfw", False)).lower()
+ encoded_query = urllib.parse.quote(current)
+ url = f"https://api.themoviedb.org/3/search/tv?api_key={api_key}&query={encoded_query}&page=1&include_adult={include_adult}"
+ async with aiohttp.ClientSession() as session:
+ data = await fetch_tmdb(url, session)
+
+ if not data or "results" not in data:
+ return []
+
+ def get_date(item: Dict[str, Any]) -> float:
+ date_str = item.get("first_air_date", "")
+ if not date_str or not isinstance(date_str, str) or len(date_str) < 4:
+ return float("-inf")
+ try:
+ year = int(date_str[:4])
+ return datetime(year=year, month=1, day=1).timestamp()
+ except (ValueError, TypeError):
+ return float("-inf")
+
+ sorted_results = sorted(data.get("results", []), key=get_date, reverse=True)
+
+ return [
+ app_commands.Choice(
+ name=f"{result.get('name', 'Unknown')} ({result.get('first_air_date', '')[:4] or 'N/A'})",
+ value=result.get("name", "Unknown"),
+ )
+ for result in sorted_results[:25]
+ ]
+
@commands.hybrid_command()
@app_commands.describe(query="The person you want to search for.")
@commands.bot_has_permissions(embed_links=True)
From 9d0468b4d6e29fcb38c8582f8d50fae8515e3e04 Mon Sep 17 00:00:00 2001
From: MAX <63972751+ltzmax@users.noreply.github.com>
Date: Sat, 10 May 2025 15:47:02 +0200
Subject: [PATCH 06/31] Ops, this gotta be there still
---
themoviedb/info.json | 2 +-
themoviedb/themoviedb.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/themoviedb/info.json b/themoviedb/info.json
index 2513f4ab..cde6af4d 100644
--- a/themoviedb/info.json
+++ b/themoviedb/info.json
@@ -3,7 +3,7 @@
"max"
],
"name": "TheMovieDB",
- "install_msg": "Thanks for installing.\nYou will need to set your API key before using this cog. See `[p]tmdbset creds`.\n for documentation.\nIf you enjoy my work, you can donate at [buymeacoffee]()",
+ "install_msg": "Thanks for installing.\nYou will need to set your API key before using this cog. See `[p]tmdbset creds`.\n for documentation.\nIf you enjoy my work, you can donate at [buymeacoffee]().\n## PLEASE NOTE:\nThis cog is in alpha phase and using Components V2 and is not fully tested or and stable enough for production use. This also require the Components V2 PR from discord.py for this cog to work.",
"description": "Search for informations of movies and TV shows from themoviedb.org.",
"short": "Search for informations of movies and TV shows from themoviedb.org.",
"hidden": false,
diff --git a/themoviedb/themoviedb.py b/themoviedb/themoviedb.py
index 8b7dc579..ccbc98f5 100644
--- a/themoviedb/themoviedb.py
+++ b/themoviedb/themoviedb.py
@@ -40,7 +40,7 @@ class TheMovieDB(commands.Cog):
"""
__author__ = "MAX"
- __version__ = "1.8.0"
+ __version__ = "2.0.0a"
__docs__ = "https://docs.maxapp.tv/"
def __init__(self, bot):
From 20c2e9f5259af749fda9bb4bd0db77ad03bfa678 Mon Sep 17 00:00:00 2001
From: MAX <63972751+ltzmax@users.noreply.github.com>
Date: Mon, 7 Jul 2025 13:58:09 +0200
Subject: [PATCH 07/31] Update to work on newest changes
It threw just This interaction failed so this fixed it. + added primary color on the buttons, looked nice.
---
themoviedb/tmdb_utils.py | 15 ++++++++-------
1 file changed, 8 insertions(+), 7 deletions(-)
diff --git a/themoviedb/tmdb_utils.py b/themoviedb/tmdb_utils.py
index 493d5806..14df3817 100644
--- a/themoviedb/tmdb_utils.py
+++ b/themoviedb/tmdb_utils.py
@@ -379,7 +379,7 @@ async def interaction_check(self, interaction: discord.Interaction):
class MediaButton(discord.ui.Button["MediaPaginator"]):
def __init__(self, index, label=None):
- super().__init__(label=label or "Select")
+ super().__init__(label=label or "Select", style=discord.ButtonStyle.primary)
self.index = index
async def _send_error(self, interaction, message, exc=None):
@@ -423,14 +423,15 @@ def __init__(self, direction):
label="Previous" if direction == "prev" else "Next",
emoji="◀️" if direction == "prev" else "▶️",
style=discord.ButtonStyle.secondary,
- custom_id=direction,
+ custom_id=f"nav_{direction}"
)
+ self.direction = direction
async def _send_error(self, interaction, message, exc=None):
"""Send an error message and log if an exception is provided."""
if exc:
log.error(
- f"Error navigating page {self.view.current_page} ({self.custom_id}): {exc}",
+ f"Error navigating page {self.view.current_page} ({self.direction}): {exc}",
exc_info=True,
)
await interaction.response.send_message(message, ephemeral=True)
@@ -439,16 +440,16 @@ async def callback(self, interaction: discord.Interaction) -> None:
try:
start_idx = self.view.current_page * self.view.items_per_page
end_idx = start_idx + self.view.items_per_page
- if self.custom_id == "prev" and self.view.current_page > 0:
+ if self.direction == "prev" and self.view.current_page > 0:
self.view.current_page -= 1
- elif self.custom_id == "next" and end_idx < len(self.view.filtered_results):
+ elif self.direction == "next" and end_idx < len(self.view.filtered_results):
self.view.current_page += 1
self.view._update_content()
await interaction.response.defer()
await self.view.message.edit(content=None, view=self.view)
- except Exception:
- await self._send_error(interaction, "Error navigating, please try again.")
+ except Exception as e:
+ await self._send_error(interaction, "Error navigating, please try again.", e)
paginator = MediaPaginator(ctx, filtered_results, media_type)
message = await ctx.send(content="", view=paginator)
From 52458ae693babf87391c8772183a51b5fbd8d8ae Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 7 Jul 2025 11:58:20 +0000
Subject: [PATCH 08/31] [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---
themoviedb/tmdb_utils.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/themoviedb/tmdb_utils.py b/themoviedb/tmdb_utils.py
index 14df3817..a4b40c0e 100644
--- a/themoviedb/tmdb_utils.py
+++ b/themoviedb/tmdb_utils.py
@@ -423,7 +423,7 @@ def __init__(self, direction):
label="Previous" if direction == "prev" else "Next",
emoji="◀️" if direction == "prev" else "▶️",
style=discord.ButtonStyle.secondary,
- custom_id=f"nav_{direction}"
+ custom_id=f"nav_{direction}",
)
self.direction = direction
From 2e4f915db5f0c6447e567ea12ca7db3f3bfc8730 Mon Sep 17 00:00:00 2001
From: MAX <63972751+ltzmax@users.noreply.github.com>
Date: Thu, 24 Jul 2025 18:31:43 +0200
Subject: [PATCH 09/31] Added possiblity to automatically post videos.
* This is from each film studios including streaming services.
---
themoviedb/themoviedb.py | 360 ++++++++++++++++++++++++++++++++++++++-
themoviedb/tmdb_utils.py | 27 ++-
2 files changed, 377 insertions(+), 10 deletions(-)
diff --git a/themoviedb/themoviedb.py b/themoviedb/themoviedb.py
index ccbc98f5..530215af 100644
--- a/themoviedb/themoviedb.py
+++ b/themoviedb/themoviedb.py
@@ -22,16 +22,21 @@
SOFTWARE.
"""
-import urllib.parse
-from datetime import datetime
-from typing import Any, Dict, List, Optional, Union
+import asyncio
+import xml.etree.ElementTree as ET
+from typing import Dict, Optional, Union
+import datetime
import aiohttp
import discord
-from redbot.core import app_commands, commands
+from discord.ext import tasks
+from red_commons.logging import getLogger
+from redbot.core import Config, app_commands, commands
from redbot.core.utils.views import SetApiView, SimpleMenu
-from .tmdb_utils import fetch_tmdb, person_embed, search_and_display
+from .tmdb_utils import PREDEFINED_CHANNELS, person_embed, search_and_display
+
+logger = getLogger("red.maxcogs.themoviedb")
class TheMovieDB(commands.Cog):
@@ -45,10 +50,31 @@ class TheMovieDB(commands.Cog):
def __init__(self, bot):
self.bot = bot
+ self.config: Config = Config.get_conf(self, identifier=1111238727729911, force_registration=True)
+ default_guild = {
+ "notification_channel": None,
+ "channels_status": {},
+ "ping_role": None,
+ }
+ self.config.register_guild(**default_guild)
self.session = aiohttp.ClientSession()
+ self.check_for_new_trailers.start()
async def cog_unload(self) -> None:
- await self.session.close()
+ """Clean up on cog unload."""
+ self.check_for_new_trailers.cancel()
+ if hasattr(self, "session") and self.session:
+ if not self.session.closed:
+ try:
+ await asyncio.wait_for(self.session.close(), timeout=5.0)
+ logger.info(f"Session closed successfully: {self.session.closed}")
+ except asyncio.TimeoutError:
+ logger.warning("Session close timed out—forcing shutdown.")
+ except discord.HTTPException as e:
+ logger.error(f"Error closing session: {e}", exc_info=True)
+ else:
+ logger.info("Session already closed.")
+ logger.info("Cog unload complete.")
def format_help_for_context(self, ctx: commands.Context) -> str:
"""Thanks Sinbad!"""
@@ -59,13 +85,333 @@ async def red_delete_data_for_user(self, **kwargs) -> None:
"""Nothing to delete."""
return
+ async def fetch_feed(self, youtube_channel_id: str) -> Optional[str]:
+ url = f"https://www.youtube.com/feeds/videos.xml?channel_id={youtube_channel_id}"
+ try:
+ async with self.session.get(url, timeout=10) as response:
+ if response.status == 200:
+ return await response.text()
+ else:
+ logger.warning(
+ f"Failed to fetch feed for channel {youtube_channel_id}: HTTP {response.status}"
+ )
+ return None
+ except aiohttp.ClientError as e:
+ logger.error(f"Error fetching feed for channel {youtube_channel_id}: {e}")
+ return None
+
+ async def check_trailers(self, guild: discord.Guild) -> None:
+ guild_data = await self.config.guild(guild).all()
+ notification_channel_id = guild_data.get("notification_channel")
+ if not notification_channel_id:
+ return
+
+ channel_to_post = guild.get_channel(notification_channel_id)
+ if not channel_to_post:
+ return
+
+ ping_role_id = guild_data.get("ping_role")
+ ping_role = guild.get_role(ping_role_id) if ping_role_id else None
+ ping_mention = f"{ping_role.mention} " if ping_role else ""
+ channels_status = guild_data.get("channels_status", {})
+
+ enabled_channels = [
+ (key, details)
+ for key, details in PREDEFINED_CHANNELS.items()
+ if channels_status.get(key, {}).get("enabled", False)
+ ]
+ if not enabled_channels:
+ return
+
+ sem = asyncio.Semaphore(5) # Limit concurrent fetches to avoid rate limiting.
+ async def fetch_with_sem(key, details):
+ async with sem:
+ return key, details, await self.fetch_feed(details["id"])
+
+ fetch_tasks = [fetch_with_sem(key, details) for key, details in enabled_channels]
+ results = await asyncio.gather(*fetch_tasks, return_exceptions=True)
+
+ updates = {}
+ for result in results:
+ if isinstance(result, Exception):
+ logger.error(f"Fetch error: {result}")
+ continue
+
+ key, details, feed_data = result
+ if not feed_data:
+ failure_count = channels_status.get(key, {}).get("failure_count", 0) + 1
+ updates[key] = {"enabled": failure_count < 3, "failure_count": failure_count}
+ if failure_count >= 3:
+ try:
+ await channel_to_post.send(
+ f"Disabled notifications for **{details['name']}** due to repeated failures (HTTP 404).\n"
+ "Please contact the bot owner to resolve this issue."
+ )
+ except (discord.Forbidden, discord.HTTPException) as e:
+ logger.error(
+ f"Failed to send disable message to {channel_to_post.name} in {guild.name}: {e}"
+ )
+ continue
+
+ updates[key] = {"enabled": True, "failure_count": 0}
+
+ try:
+ root = ET.fromstring(feed_data)
+ entries = root.findall("{http://www.w3.org/2005/Atom}entry")
+ if not entries:
+ logger.debug(f"No entries in feed for {details['name']}")
+ continue
+
+ latest_video = entries[0]
+ video_id_elem = latest_video.find(
+ "{http://www.youtube.com/xml/schemas/2015}videoId"
+ )
+ if video_id_elem is None:
+ logger.warning(f"No video ID in latest entry for {details['name']}")
+ continue
+
+ video_id = video_id_elem.text
+ last_video_id = channels_status.get(key, {}).get("last_video_id")
+
+ published_elem = latest_video.find("{http://www.w3.org/2005/Atom}published")
+ if published_elem is None:
+ logger.warning(f"No published date in latest entry for {details['name']}")
+ continue
+
+ published_str = published_elem.text.replace("Z", "+00:00")
+ published_dt = datetime.datetime.fromisoformat(published_str)
+ published_ts = published_dt.timestamp()
+ last_published_ts = channels_status.get(key, {}).get("last_published_ts", 0)
+
+ if last_published_ts == 0:
+ updates[key]["last_published_ts"] = published_ts
+ updates[key]["last_video_id"] = video_id
+ continue
+
+ if published_ts <= last_published_ts or video_id == last_video_id:
+ logger.debug(f"No new video for {details['name']}")
+ continue
+
+ updates[key]["last_published_ts"] = published_ts
+ updates[key]["last_video_id"] = video_id
+ video_url_elem = latest_video.find("{http://www.w3.org/2005/Atom}link")
+ if video_url_elem is None:
+ logger.warning(f"No link in latest entry for {details['name']}")
+ continue
+
+ video_url = video_url_elem.attrib["href"]
+ author_name = (
+ root.findtext(
+ "{http://www.w3.org/2005/Atom}author/{http://www.w3.org/2005/Atom}name"
+ )
+ or details["name"]
+ )
+ message = (
+ f"{ping_mention}**{author_name}** has uploaded a new video!\n{video_url}"
+ ).strip()
+ try:
+ await channel_to_post.send(message)
+ except (discord.Forbidden, discord.HTTPException) as e:
+ logger.error(
+ f"Failed to send notification to {channel_to_post.name} in {guild.name}: {e}"
+ )
+ except ET.ParseError as e:
+ logger.error(f"Failed to parse RSS feed for {details['name']}: {e}")
+ continue
+ except discord.HTTPException as e:
+ logger.error(f"Unexpected error processing feed for {details['name']}: {e}")
+ continue
+
+ if updates:
+ try:
+ async with self.config.guild(guild).channels_status() as statuses:
+ for key, data in updates.items():
+ if key not in statuses:
+ statuses[key] = {"enabled": True, "failure_count": 0}
+ statuses[key].update(data)
+ except discord.HTTPException as e:
+ logger.error(f"Failed to update channels_status for guild {guild.id}: {e}")
+
+ @tasks.loop(minutes=5)
+ async def check_for_new_trailers(self) -> None:
+ all_guilds = await self.config.all_guilds()
+ for guild_id in all_guilds:
+ guild = self.bot.get_guild(guild_id)
+ if not guild:
+ continue
+ try:
+ await self.check_trailers(guild)
+ except discord.HTTPException as e:
+ logger.error(f"Error checking video for guild {guild.id}: {e}", exc_info=True)
+
+ @check_for_new_trailers.before_loop
+ async def before_check_for_new_trailers(self) -> None:
+ await self.bot.wait_until_ready()
+
@commands.group()
- @commands.is_owner()
+ @commands.admin_or_permissions(manage_guild=True)
async def tmdbset(self, ctx: commands.Context):
"""
Configure TheMovieDB cog settings.
"""
+ @tmdbset.command(name="channel")
+ async def set_channel(
+ self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None
+ ) -> None:
+ """Set or unset the channel for video notifications."""
+ guild_data = await self.config.guild(ctx.guild).all()
+ channels_status = guild_data.get("channels_status", {})
+
+ any_enabled = any(
+ status.get("enabled", False) for status in channels_status.values()
+ )
+
+ if channel:
+ if not channel.permissions_for(ctx.me).send_messages:
+ return await ctx.send(
+ f"I don't have permission to send messages in {channel.mention}. Please choose another channel or fix permissions."
+ )
+
+ await self.config.guild(ctx.guild).notification_channel.set(channel.id)
+ msg = f"Video notifications will now be sent to {channel.mention}."
+ if not any_enabled:
+ msg += (
+ f"\nPlease enable at least one studio with `{ctx.clean_prefix}tmdbset toggle `.\n"
+ f"Use `{ctx.clean_prefix}tmdbset list` to see available studios."
+ )
+ await ctx.send(msg)
+ else:
+ await self.config.guild(ctx.guild).notification_channel.set(None)
+ msg = "video notifications have been disabled."
+ if any_enabled:
+ msg += "\n(Any enabled studios will remain configured but won't notify until a channel is set again.)"
+ await ctx.send(msg)
+
+ @tmdbset.command(name="toggle")
+ async def toggle_channel(self, ctx: commands.Context, *channel_names: str) -> None:
+ """
+ Toggle notifications for one or more studios, or all studios.
+
+ Use `[p]tmdbset list` to see available studios. Pass 'all' to toggle all studios,
+ or specify multiple studio names to toggle them at once.
+
+ **NOTE**:
+ Videos may contain more than just a trailer from a movie or tv show, such as behind the scenes content or interviews. This is intended to keep you updated on new content from your favorite studios and channels. Do note that youtube shorts are not ignored, so you may receive notifications for them as well.
+
+ **Examples**:
+ - `[p]tmdbset toggle marvel`
+ - `[p]tmdbset toggle netflix sony amazon`
+ - `[p]tmdbset toggle all`
+
+ **Arguments**:
+ - ``: One or more studio names to toggle, or 'all' to toggle all studios.
+ """
+ if not channel_names:
+ return await ctx.send(
+ f"Please provide at least one studio name or `all`. Use `{ctx.clean_prefix}tmdbset list` to see options."
+ )
+
+ if "all" in [name.lower() for name in channel_names]:
+ if len(channel_names) > 1:
+ return await ctx.send(
+ f"Cannot combine 'all' with specific studio names. Use `{ctx.clean_prefix}tmdbset toggle all` or list specific studios."
+ )
+
+ channel_keys = list(PREDEFINED_CHANNELS.keys())
+ else:
+ channel_keys = [name.lower() for name in channel_names]
+
+ valid_toggles = []
+ invalid_names = []
+ async with self.config.guild(ctx.guild).channels_status() as statuses:
+ for key in channel_keys:
+ if key not in PREDEFINED_CHANNELS:
+ invalid_names.append(key)
+ continue
+ if key not in statuses:
+ statuses[key] = {"enabled": True, "failure_count": 0}
+ else:
+ statuses[key]["enabled"] = not statuses[key].get("enabled", False)
+ statuses[key]["failure_count"] = 0
+ status = "enabled" if statuses[key]["enabled"] else "disabled"
+ valid_toggles.append(f"{PREDEFINED_CHANNELS[key]['name']} (`{key}`): **{status}**")
+
+ response = ""
+ if valid_toggles:
+ response += (
+ "Toggled notifications:\n"
+ + "\n".join(f"- {toggle}" for toggle in valid_toggles)
+ + "\n"
+ )
+ if invalid_names:
+ response += (
+ "\nInvalid studio names: "
+ + ", ".join(f"`{name}`" for name in invalid_names)
+ + f". Use `{ctx.clean_prefix}tmdbset list` to see options."
+ )
+
+ if response:
+ await ctx.send(response.strip())
+ else:
+ await ctx.send(
+ f"No valid studios toggled. Use `{ctx.clean_prefix}tmdbset list` to see options."
+ )
+
+ @tmdbset.command(name="list")
+ async def list_channels(self, ctx: commands.Context) -> None:
+ """List all available studios and their notification status."""
+ guild_data = await self.config.guild(ctx.guild).all()
+ notification_channel_id = guild_data.get("notification_channel")
+ ping_role_id = guild_data.get("ping_role")
+
+ if notification_channel_id:
+ channel = ctx.guild.get_channel(notification_channel_id)
+ msg = f"Notification Channel: {channel.mention if channel else 'Not Set'}\n"
+ else:
+ msg = "Notification Channel: Not Set\n"
+
+ if ping_role_id:
+ role = ctx.guild.get_role(ping_role_id)
+ msg += f"Ping Role: {role.mention if role else 'Not Set'}\n"
+ else:
+ msg += "Ping Role: Not Set\n"
+
+ msg += "\nAvailable Studios:\n"
+ channels_status = guild_data.get("channels_status", {})
+ for key, details in PREDEFINED_CHANNELS.items():
+ status = (
+ "Enabled" if channels_status.get(key, {}).get("enabled", False) else "Disabled"
+ )
+ msg += f"- {details['name']} (`{key}`): **{status}**\n"
+
+ pages = []
+ current_page = ""
+ for line in msg.splitlines(keepends=True):
+ if len(current_page) + len(line) > 1900:
+ pages.append(current_page)
+ current_page = line
+ else:
+ current_page += line
+ if current_page:
+ pages.append(current_page)
+ await SimpleMenu(pages, disable_after_timeout=True, timeout=120).start(ctx)
+
+ @tmdbset.command(name="role")
+ async def set_role(self, ctx: commands.Context, role: Optional[discord.Role] = None) -> None:
+ """Set or unset a role to ping for new video notifications."""
+ if role:
+ if role >= ctx.guild.me.top_role:
+ return await ctx.send("That role is higher than my highest role.")
+ if role.is_default() or role.is_everyone() or role.name == "@here":
+ return await ctx.send("Cannot set `@everyone` or `@here` as ping roles.")
+ await self.config.guild(ctx.guild).ping_role.set(role.id)
+ await ctx.send(f"New video notifications will now ping {role.mention}.")
+ else:
+ await self.config.guild(ctx.guild).ping_role.set(None)
+ await ctx.send("Ping role for video notifications has been disabled ")
+
+ @commands.is_owner()
@tmdbset.command(name="creds")
@commands.bot_has_permissions(embed_links=True)
async def tmdbset_creds(self, ctx: commands.Context):
diff --git a/themoviedb/tmdb_utils.py b/themoviedb/tmdb_utils.py
index a4b40c0e..31e39dc1 100644
--- a/themoviedb/tmdb_utils.py
+++ b/themoviedb/tmdb_utils.py
@@ -31,13 +31,34 @@
import aiohttp
import discord
import orjson
-from redbot.core.bot import Red
+from red_commons.logging import getLogger
from redbot.core.utils.chat_formatting import box, header, humanize_list, humanize_number
from redbot.core.utils.views import SimpleMenu
-log = logging.getLogger("red.maxcogs.themoviedb.tmdb_utils")
+log = getLogger("red.maxcogs.themoviedb.tmdb_utils")
BASE_MEDIA = "https://api.themoviedb.org/3/search"
BASE_URL = "https://api.themoviedb.org/3"
+PREDEFINED_CHANNELS: Dict[str, Dict[str, str]] = {
+ "marvel": {"id": "UCvC4D8onUfXzvjTOM-dBfEA", "name": "Marvel Entertainment"},
+ "dc": {"id": "UCiifkYAs_bq1pt_zbNAzYGg", "name": "DC Official"},
+ "pixar": {"id": "UC_IRYSp4auq7hKLvziWVH6w", "name": "Pixar"},
+ "disney": {"id": "UC_5niPa-d35gg88HaS7RrIw", "name": "Disney"},
+ "disneyplus": {"id": "UCIrgJInjLS2BhlHOMDW7v0g", "name": "Disney+"},
+ "illumination": {"id": "UCq7OHvWO6Z3u-LztFdrcU-g", "name": "Illumination Entertainment"},
+ "warnerbros": {"id": "UCjmJDM5pRKbUlVIzDYYWb6g", "name": "Warner Bros. Pictures"},
+ "sony": {"id": "UCz97F7dMxBNOfGYu3rx8aCw", "name": "Sony Pictures Entertainment"},
+ "sonyanimation": {"id": "UCnLuLSV-Oi0ctqjxGgxFlmg", "name": "Sony Pictures Animation"},
+ "universal": {"id": "UCq0OueAsdxH6b8nyAspwViw", "name": "Universal Pictures"},
+ "paramount": {"id": "UCF9imwPMSGz4Vq1NiTWCC7g", "name": "Paramount Pictures"},
+ "20thcentury": {"id": "UC2-BeLxzUBSs0uSrmzWhJuQ", "name": "20th Century Studios"},
+ "lionsgate": {"id": "UCJ6nMHaJPZvsJ-HmUmj1SeA", "name": "Lionsgate Movies"},
+ "a24": {"id": "UCuPivVjnfNo4mb3Oog_frZg", "name": "A24"},
+ "hbomax": {"id": "UCx-KWLTKlB83hDI6UKECtJQ", "name": "HBO Max (formerly max)"},
+ "netflix": {"id": "UCWOA1ZGywLbqmigxE4Qlvuw", "name": "Netflix"},
+ "appletv": {"id": "UC1Myj674wRVXB9I4c6Hm5zA", "name": "Apple TV"},
+ "amazon": {"id": "UCQJWtTnAHhEG5w4uN0udnUQ", "name": "Amazon Prime Video"},
+ "mgm": {"id": "UCf5CjDJvsFvtVIhkfmKAwAA", "name": "Metro-Goldwyn-Mayer (MGM)"},
+}
async def fetch_tmdb(url: str, session: aiohttp.ClientSession) -> Optional[Dict[str, Any]]:
@@ -48,7 +69,7 @@ async def fetch_tmdb(url: str, session: aiohttp.ClientSession) -> Optional[Dict[
log.error(f"TMDB request failed with status: {response.status}")
return None
return orjson.loads(await response.read())
- except Exception as e:
+ except discord.HTTPException as e:
log.error(f"TMDB request error: {e}")
return None
From a98757390051f3f734399ac43cbbeb69536044c7 Mon Sep 17 00:00:00 2001
From: MAX <63972751+ltzmax@users.noreply.github.com>
Date: Thu, 24 Jul 2025 18:32:41 +0200
Subject: [PATCH 10/31] Update themoviedb.py
---
themoviedb/themoviedb.py | 125 +++++++++++++++++++++++++++++++++++++++
1 file changed, 125 insertions(+)
diff --git a/themoviedb/themoviedb.py b/themoviedb/themoviedb.py
index 530215af..aeef9755 100644
--- a/themoviedb/themoviedb.py
+++ b/themoviedb/themoviedb.py
@@ -56,6 +56,7 @@ def __init__(self, bot):
"channels_status": {},
"ping_role": None,
}
+ self.config.register_global(**default_global)
self.config.register_guild(**default_guild)
self.session = aiohttp.ClientSession()
self.check_for_new_trailers.start()
@@ -411,6 +412,130 @@ async def set_role(self, ctx: commands.Context, role: Optional[discord.Role] = N
await self.config.guild(ctx.guild).ping_role.set(None)
await ctx.send("Ping role for video notifications has been disabled ")
+ @commands.is_owner()
+ @tmdbset.command(name="usebox")
+ async def tmdbset_usebox(self, ctx: commands.Context, value: bool):
+ """
+ Toggle notifications for one or more studios, or all studios.
+
+ Use `[p]tmdbset list` to see available studios. Pass 'all' to toggle all studios,
+ or specify multiple studio names to toggle them at once.
+
+ **NOTE**:
+ Videos may contain more than just a trailer from a movie or tv show, such as behind the scenes content or interviews. This is intended to keep you updated on new content from your favorite studios and channels. Do note that youtube shorts are not ignored, so you may receive notifications for them as well.
+
+ **Examples**:
+ - `[p]tmdbset toggle marvel`
+ - `[p]tmdbset toggle netflix sony amazon`
+ - `[p]tmdbset toggle all`
+
+ **Arguments**:
+ - ``: One or more studio names to toggle, or 'all' to toggle all studios.
+ """
+ if not channel_names:
+ return await ctx.send(
+ f"Please provide at least one studio name or `all`. Use `{ctx.clean_prefix}tmdbset list` to see options."
+ )
+
+ if "all" in [name.lower() for name in channel_names]:
+ if len(channel_names) > 1:
+ return await ctx.send(
+ f"Cannot combine 'all' with specific studio names. Use `{ctx.clean_prefix}tmdbset toggle all` or list specific studios."
+ )
+
+ channel_keys = list(PREDEFINED_CHANNELS.keys())
+ else:
+ channel_keys = [name.lower() for name in channel_names]
+
+ valid_toggles = []
+ invalid_names = []
+ async with self.config.guild(ctx.guild).channels_status() as statuses:
+ for key in channel_keys:
+ if key not in PREDEFINED_CHANNELS:
+ invalid_names.append(key)
+ continue
+ if key not in statuses:
+ statuses[key] = {"enabled": True, "failure_count": 0}
+ else:
+ statuses[key]["enabled"] = not statuses[key].get("enabled", False)
+ statuses[key]["failure_count"] = 0
+ status = "enabled" if statuses[key]["enabled"] else "disabled"
+ valid_toggles.append(f"{PREDEFINED_CHANNELS[key]['name']} (`{key}`): **{status}**")
+
+ response = ""
+ if valid_toggles:
+ response += (
+ "Toggled notifications:\n"
+ + "\n".join(f"- {toggle}" for toggle in valid_toggles)
+ + "\n"
+ )
+ if invalid_names:
+ response += (
+ "\nInvalid studio names: "
+ + ", ".join(f"`{name}`" for name in invalid_names)
+ + f". Use `{ctx.clean_prefix}tmdbset list` to see options."
+ )
+
+ if response:
+ await ctx.send(response.strip())
+ else:
+ await ctx.send(
+ f"No valid studios toggled. Use `{ctx.clean_prefix}tmdbset list` to see options."
+ )
+
+ @tmdbset.command(name="list")
+ async def list_channels(self, ctx: commands.Context) -> None:
+ """List all available studios and their notification status."""
+ guild_data = await self.config.guild(ctx.guild).all()
+ notification_channel_id = guild_data.get("notification_channel")
+ ping_role_id = guild_data.get("ping_role")
+
+ if notification_channel_id:
+ channel = ctx.guild.get_channel(notification_channel_id)
+ msg = f"Notification Channel: {channel.mention if channel else 'Not Set'}\n"
+ else:
+ msg = "Notification Channel: Not Set\n"
+
+ if ping_role_id:
+ role = ctx.guild.get_role(ping_role_id)
+ msg += f"Ping Role: {role.mention if role else 'Not Set'}\n"
+ else:
+ msg += "Ping Role: Not Set\n"
+
+ msg += "\nAvailable Studios:\n"
+ channels_status = guild_data.get("channels_status", {})
+ for key, details in PREDEFINED_CHANNELS.items():
+ status = (
+ "Enabled" if channels_status.get(key, {}).get("enabled", False) else "Disabled"
+ )
+ msg += f"- {details['name']} (`{key}`): **{status}**\n"
+
+ pages = []
+ current_page = ""
+ for line in msg.splitlines(keepends=True):
+ if len(current_page) + len(line) > 1900:
+ pages.append(current_page)
+ current_page = line
+ else:
+ current_page += line
+ if current_page:
+ pages.append(current_page)
+ await SimpleMenu(pages, disable_after_timeout=True, timeout=120).start(ctx)
+
+ @tmdbset.command(name="role")
+ async def set_role(self, ctx: commands.Context, role: Optional[discord.Role] = None) -> None:
+ """Set or unset a role to ping for new video notifications."""
+ if role:
+ if role >= ctx.guild.me.top_role:
+ return await ctx.send("That role is higher than my highest role.")
+ if role.is_default() or role.is_everyone() or role.name == "@here":
+ return await ctx.send("Cannot set `@everyone` or `@here` as ping roles.")
+ await self.config.guild(ctx.guild).ping_role.set(role.id)
+ await ctx.send(f"New video notifications will now ping {role.mention}.")
+ else:
+ await self.config.guild(ctx.guild).ping_role.set(None)
+ await ctx.send("Ping role for video notifications has been disabled ")
+
@commands.is_owner()
@tmdbset.command(name="creds")
@commands.bot_has_permissions(embed_links=True)
From 6640895e4cc597f1f13021d844c1e1a5707afcc2 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Thu, 24 Jul 2025 16:32:48 +0000
Subject: [PATCH 11/31] [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---
themoviedb/themoviedb.py | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/themoviedb/themoviedb.py b/themoviedb/themoviedb.py
index aeef9755..2d33f5e9 100644
--- a/themoviedb/themoviedb.py
+++ b/themoviedb/themoviedb.py
@@ -23,10 +23,10 @@
"""
import asyncio
+import datetime
import xml.etree.ElementTree as ET
from typing import Dict, Optional, Union
-import datetime
import aiohttp
import discord
from discord.ext import tasks
@@ -50,7 +50,9 @@ class TheMovieDB(commands.Cog):
def __init__(self, bot):
self.bot = bot
- self.config: Config = Config.get_conf(self, identifier=1111238727729911, force_registration=True)
+ self.config: Config = Config.get_conf(
+ self, identifier=1111238727729911, force_registration=True
+ )
default_guild = {
"notification_channel": None,
"channels_status": {},
@@ -125,6 +127,7 @@ async def check_trailers(self, guild: discord.Guild) -> None:
return
sem = asyncio.Semaphore(5) # Limit concurrent fetches to avoid rate limiting.
+
async def fetch_with_sem(key, details):
async with sem:
return key, details, await self.fetch_feed(details["id"])
@@ -264,9 +267,7 @@ async def set_channel(
guild_data = await self.config.guild(ctx.guild).all()
channels_status = guild_data.get("channels_status", {})
- any_enabled = any(
- status.get("enabled", False) for status in channels_status.values()
- )
+ any_enabled = any(status.get("enabled", False) for status in channels_status.values())
if channel:
if not channel.permissions_for(ctx.me).send_messages:
From 6c338e690ce2800838340c4bbcbc87d7a2c3a776 Mon Sep 17 00:00:00 2001
From: MAX <63972751+ltzmax@users.noreply.github.com>
Date: Thu, 24 Jul 2025 18:33:19 +0200
Subject: [PATCH 12/31] Update themoviedb.py
---
themoviedb/themoviedb.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/themoviedb/themoviedb.py b/themoviedb/themoviedb.py
index aeef9755..bdcfcd92 100644
--- a/themoviedb/themoviedb.py
+++ b/themoviedb/themoviedb.py
@@ -56,6 +56,11 @@ def __init__(self, bot):
"channels_status": {},
"ping_role": None,
}
+ default_guild = {
+ "notification_channel": None,
+ "channels_status": {},
+ "ping_role": None,
+ }
self.config.register_global(**default_global)
self.config.register_guild(**default_guild)
self.session = aiohttp.ClientSession()
From 0e932a0e9bdcf729f30129f3597b843353cf561d Mon Sep 17 00:00:00 2001
From: MAX <63972751+ltzmax@users.noreply.github.com>
Date: Thu, 24 Jul 2025 18:33:47 +0200
Subject: [PATCH 13/31] Update themoviedb.py
---
themoviedb/themoviedb.py | 6 ------
1 file changed, 6 deletions(-)
diff --git a/themoviedb/themoviedb.py b/themoviedb/themoviedb.py
index 2304da68..8312ddc8 100644
--- a/themoviedb/themoviedb.py
+++ b/themoviedb/themoviedb.py
@@ -58,12 +58,6 @@ def __init__(self, bot):
"channels_status": {},
"ping_role": None,
}
- default_guild = {
- "notification_channel": None,
- "channels_status": {},
- "ping_role": None,
- }
- self.config.register_global(**default_global)
self.config.register_guild(**default_guild)
self.session = aiohttp.ClientSession()
self.check_for_new_trailers.start()
From 14ed085d5d7bb5acf3c1be609c672e3ea0e80547 Mon Sep 17 00:00:00 2001
From: MAX <63972751+ltzmax@users.noreply.github.com>
Date: Thu, 24 Jul 2025 18:35:02 +0200
Subject: [PATCH 14/31] Update themoviedb.py
---
themoviedb/themoviedb.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/themoviedb/themoviedb.py b/themoviedb/themoviedb.py
index 8312ddc8..2063eda0 100644
--- a/themoviedb/themoviedb.py
+++ b/themoviedb/themoviedb.py
@@ -25,7 +25,7 @@
import asyncio
import datetime
import xml.etree.ElementTree as ET
-from typing import Dict, Optional, Union
+from typing import Any, Dict, Optional, Union
import aiohttp
import discord
From 298ccb1e42ae6f1fab85159d155f169dc8bef7b6 Mon Sep 17 00:00:00 2001
From: MAX <63972751+ltzmax@users.noreply.github.com>
Date: Thu, 24 Jul 2025 18:36:23 +0200
Subject: [PATCH 15/31] Smh
---
themoviedb/themoviedb.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/themoviedb/themoviedb.py b/themoviedb/themoviedb.py
index 2063eda0..856cc8bc 100644
--- a/themoviedb/themoviedb.py
+++ b/themoviedb/themoviedb.py
@@ -22,6 +22,7 @@
SOFTWARE.
"""
+import urllib.parse
import asyncio
import datetime
import xml.etree.ElementTree as ET
@@ -34,7 +35,7 @@
from redbot.core import Config, app_commands, commands
from redbot.core.utils.views import SetApiView, SimpleMenu
-from .tmdb_utils import PREDEFINED_CHANNELS, person_embed, search_and_display
+from .tmdb_utils import PREDEFINED_CHANNELS, person_embed, search_and_display, fetch_tmdb
logger = getLogger("red.maxcogs.themoviedb")
From f4e3d475ed319da53b52c5ccaf8d679205708a39 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Thu, 24 Jul 2025 16:36:23 +0000
Subject: [PATCH 16/31] [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---
themoviedb/themoviedb.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/themoviedb/themoviedb.py b/themoviedb/themoviedb.py
index 856cc8bc..5b7ff79d 100644
--- a/themoviedb/themoviedb.py
+++ b/themoviedb/themoviedb.py
@@ -22,9 +22,9 @@
SOFTWARE.
"""
-import urllib.parse
import asyncio
import datetime
+import urllib.parse
import xml.etree.ElementTree as ET
from typing import Any, Dict, Optional, Union
@@ -35,7 +35,7 @@
from redbot.core import Config, app_commands, commands
from redbot.core.utils.views import SetApiView, SimpleMenu
-from .tmdb_utils import PREDEFINED_CHANNELS, person_embed, search_and_display, fetch_tmdb
+from .tmdb_utils import PREDEFINED_CHANNELS, fetch_tmdb, person_embed, search_and_display
logger = getLogger("red.maxcogs.themoviedb")
From 68d43ecaf383f90fb12e466a03ae0e6edc063f2f Mon Sep 17 00:00:00 2001
From: MAX <63972751+ltzmax@users.noreply.github.com>
Date: Thu, 24 Jul 2025 18:36:49 +0200
Subject: [PATCH 17/31] Update themoviedb.py
---
themoviedb/themoviedb.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/themoviedb/themoviedb.py b/themoviedb/themoviedb.py
index 856cc8bc..5b7ff79d 100644
--- a/themoviedb/themoviedb.py
+++ b/themoviedb/themoviedb.py
@@ -22,9 +22,9 @@
SOFTWARE.
"""
-import urllib.parse
import asyncio
import datetime
+import urllib.parse
import xml.etree.ElementTree as ET
from typing import Any, Dict, Optional, Union
@@ -35,7 +35,7 @@
from redbot.core import Config, app_commands, commands
from redbot.core.utils.views import SetApiView, SimpleMenu
-from .tmdb_utils import PREDEFINED_CHANNELS, person_embed, search_and_display, fetch_tmdb
+from .tmdb_utils import PREDEFINED_CHANNELS, fetch_tmdb, person_embed, search_and_display
logger = getLogger("red.maxcogs.themoviedb")
From d3ed9d12aa5f19bd51b487eba4c8cc2cc4dd401c Mon Sep 17 00:00:00 2001
From: MAX <63972751+ltzmax@users.noreply.github.com>
Date: Thu, 24 Jul 2025 18:37:27 +0200
Subject: [PATCH 18/31] ....
---
themoviedb/themoviedb.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/themoviedb/themoviedb.py b/themoviedb/themoviedb.py
index 5b7ff79d..3476f6da 100644
--- a/themoviedb/themoviedb.py
+++ b/themoviedb/themoviedb.py
@@ -26,7 +26,7 @@
import datetime
import urllib.parse
import xml.etree.ElementTree as ET
-from typing import Any, Dict, Optional, Union
+from typing import Any, Dict, List, Optional, Union
import aiohttp
import discord
From ac9776da0f78f7a3992ac3153cbc5d14d270756f Mon Sep 17 00:00:00 2001
From: MAX <63972751+ltzmax@users.noreply.github.com>
Date: Thu, 24 Jul 2025 18:39:19 +0200
Subject: [PATCH 19/31] The conflict is crazy man lol
---
themoviedb/themoviedb.py | 71 ----------------------------------------
1 file changed, 71 deletions(-)
diff --git a/themoviedb/themoviedb.py b/themoviedb/themoviedb.py
index 3476f6da..1497b9c6 100644
--- a/themoviedb/themoviedb.py
+++ b/themoviedb/themoviedb.py
@@ -413,77 +413,6 @@ async def set_role(self, ctx: commands.Context, role: Optional[discord.Role] = N
await self.config.guild(ctx.guild).ping_role.set(None)
await ctx.send("Ping role for video notifications has been disabled ")
- @commands.is_owner()
- @tmdbset.command(name="usebox")
- async def tmdbset_usebox(self, ctx: commands.Context, value: bool):
- """
- Toggle notifications for one or more studios, or all studios.
-
- Use `[p]tmdbset list` to see available studios. Pass 'all' to toggle all studios,
- or specify multiple studio names to toggle them at once.
-
- **NOTE**:
- Videos may contain more than just a trailer from a movie or tv show, such as behind the scenes content or interviews. This is intended to keep you updated on new content from your favorite studios and channels. Do note that youtube shorts are not ignored, so you may receive notifications for them as well.
-
- **Examples**:
- - `[p]tmdbset toggle marvel`
- - `[p]tmdbset toggle netflix sony amazon`
- - `[p]tmdbset toggle all`
-
- **Arguments**:
- - ``: One or more studio names to toggle, or 'all' to toggle all studios.
- """
- if not channel_names:
- return await ctx.send(
- f"Please provide at least one studio name or `all`. Use `{ctx.clean_prefix}tmdbset list` to see options."
- )
-
- if "all" in [name.lower() for name in channel_names]:
- if len(channel_names) > 1:
- return await ctx.send(
- f"Cannot combine 'all' with specific studio names. Use `{ctx.clean_prefix}tmdbset toggle all` or list specific studios."
- )
-
- channel_keys = list(PREDEFINED_CHANNELS.keys())
- else:
- channel_keys = [name.lower() for name in channel_names]
-
- valid_toggles = []
- invalid_names = []
- async with self.config.guild(ctx.guild).channels_status() as statuses:
- for key in channel_keys:
- if key not in PREDEFINED_CHANNELS:
- invalid_names.append(key)
- continue
- if key not in statuses:
- statuses[key] = {"enabled": True, "failure_count": 0}
- else:
- statuses[key]["enabled"] = not statuses[key].get("enabled", False)
- statuses[key]["failure_count"] = 0
- status = "enabled" if statuses[key]["enabled"] else "disabled"
- valid_toggles.append(f"{PREDEFINED_CHANNELS[key]['name']} (`{key}`): **{status}**")
-
- response = ""
- if valid_toggles:
- response += (
- "Toggled notifications:\n"
- + "\n".join(f"- {toggle}" for toggle in valid_toggles)
- + "\n"
- )
- if invalid_names:
- response += (
- "\nInvalid studio names: "
- + ", ".join(f"`{name}`" for name in invalid_names)
- + f". Use `{ctx.clean_prefix}tmdbset list` to see options."
- )
-
- if response:
- await ctx.send(response.strip())
- else:
- await ctx.send(
- f"No valid studios toggled. Use `{ctx.clean_prefix}tmdbset list` to see options."
- )
-
@tmdbset.command(name="list")
async def list_channels(self, ctx: commands.Context) -> None:
"""List all available studios and their notification status."""
From c83dbe91d2bcb294b9f03890080216c63f1473ff Mon Sep 17 00:00:00 2001
From: MAX <63972751+ltzmax@users.noreply.github.com>
Date: Sun, 10 Aug 2025 12:38:14 +0200
Subject: [PATCH 20/31] skip youtube shorts + remove dublicates.
---
themoviedb/themoviedb.py | 70 ++++++----------------------------------
1 file changed, 10 insertions(+), 60 deletions(-)
diff --git a/themoviedb/themoviedb.py b/themoviedb/themoviedb.py
index 1497b9c6..764dbe66 100644
--- a/themoviedb/themoviedb.py
+++ b/themoviedb/themoviedb.py
@@ -126,8 +126,7 @@ async def check_trailers(self, guild: discord.Guild) -> None:
if not enabled_channels:
return
- sem = asyncio.Semaphore(5) # Limit concurrent fetches to avoid rate limiting.
-
+ sem = asyncio.Semaphore(5)
async def fetch_with_sem(key, details):
async with sem:
return key, details, await self.fetch_feed(details["id"])
@@ -155,8 +154,7 @@ async def fetch_with_sem(key, details):
logger.error(
f"Failed to send disable message to {channel_to_post.name} in {guild.name}: {e}"
)
- continue
-
+ continue
updates[key] = {"enabled": True, "failure_count": 0}
try:
@@ -196,14 +194,19 @@ async def fetch_with_sem(key, details):
logger.debug(f"No new video for {details['name']}")
continue
- updates[key]["last_published_ts"] = published_ts
- updates[key]["last_video_id"] = video_id
video_url_elem = latest_video.find("{http://www.w3.org/2005/Atom}link")
if video_url_elem is None:
logger.warning(f"No link in latest entry for {details['name']}")
continue
video_url = video_url_elem.attrib["href"]
+ # Skip YouTube Shorts
+ if "/shorts/" in video_url:
+ logger.info(f"Skipping YouTube Short for {details['name']}: {video_url}")
+ continue
+
+ updates[key]["last_published_ts"] = published_ts
+ updates[key]["last_video_id"] = video_id
author_name = (
root.findtext(
"{http://www.w3.org/2005/Atom}author/{http://www.w3.org/2005/Atom}name"
@@ -299,7 +302,7 @@ async def toggle_channel(self, ctx: commands.Context, *channel_names: str) -> No
or specify multiple studio names to toggle them at once.
**NOTE**:
- Videos may contain more than just a trailer from a movie or tv show, such as behind the scenes content or interviews. This is intended to keep you updated on new content from your favorite studios and channels. Do note that youtube shorts are not ignored, so you may receive notifications for them as well.
+ Videos may contain more than just a trailer from a movie or tv show, such as behind the scenes content or interviews. This is intended to keep you updated on new content from your favorite studios and channels.
**Examples**:
- `[p]tmdbset toggle marvel`
@@ -413,59 +416,6 @@ async def set_role(self, ctx: commands.Context, role: Optional[discord.Role] = N
await self.config.guild(ctx.guild).ping_role.set(None)
await ctx.send("Ping role for video notifications has been disabled ")
- @tmdbset.command(name="list")
- async def list_channels(self, ctx: commands.Context) -> None:
- """List all available studios and their notification status."""
- guild_data = await self.config.guild(ctx.guild).all()
- notification_channel_id = guild_data.get("notification_channel")
- ping_role_id = guild_data.get("ping_role")
-
- if notification_channel_id:
- channel = ctx.guild.get_channel(notification_channel_id)
- msg = f"Notification Channel: {channel.mention if channel else 'Not Set'}\n"
- else:
- msg = "Notification Channel: Not Set\n"
-
- if ping_role_id:
- role = ctx.guild.get_role(ping_role_id)
- msg += f"Ping Role: {role.mention if role else 'Not Set'}\n"
- else:
- msg += "Ping Role: Not Set\n"
-
- msg += "\nAvailable Studios:\n"
- channels_status = guild_data.get("channels_status", {})
- for key, details in PREDEFINED_CHANNELS.items():
- status = (
- "Enabled" if channels_status.get(key, {}).get("enabled", False) else "Disabled"
- )
- msg += f"- {details['name']} (`{key}`): **{status}**\n"
-
- pages = []
- current_page = ""
- for line in msg.splitlines(keepends=True):
- if len(current_page) + len(line) > 1900:
- pages.append(current_page)
- current_page = line
- else:
- current_page += line
- if current_page:
- pages.append(current_page)
- await SimpleMenu(pages, disable_after_timeout=True, timeout=120).start(ctx)
-
- @tmdbset.command(name="role")
- async def set_role(self, ctx: commands.Context, role: Optional[discord.Role] = None) -> None:
- """Set or unset a role to ping for new video notifications."""
- if role:
- if role >= ctx.guild.me.top_role:
- return await ctx.send("That role is higher than my highest role.")
- if role.is_default() or role.is_everyone() or role.name == "@here":
- return await ctx.send("Cannot set `@everyone` or `@here` as ping roles.")
- await self.config.guild(ctx.guild).ping_role.set(role.id)
- await ctx.send(f"New video notifications will now ping {role.mention}.")
- else:
- await self.config.guild(ctx.guild).ping_role.set(None)
- await ctx.send("Ping role for video notifications has been disabled ")
-
@commands.is_owner()
@tmdbset.command(name="creds")
@commands.bot_has_permissions(embed_links=True)
From 7a4265775e8d570f46186eac2d5a1335900df73e Mon Sep 17 00:00:00 2001
From: MAX <63972751+ltzmax@users.noreply.github.com>
Date: Tue, 19 Aug 2025 18:39:32 +0200
Subject: [PATCH 21/31] add crunchyroll
---
themoviedb/themoviedb.py | 1 +
themoviedb/tmdb_utils.py | 1 +
2 files changed, 2 insertions(+)
diff --git a/themoviedb/themoviedb.py b/themoviedb/themoviedb.py
index 764dbe66..9e612635 100644
--- a/themoviedb/themoviedb.py
+++ b/themoviedb/themoviedb.py
@@ -127,6 +127,7 @@ async def check_trailers(self, guild: discord.Guild) -> None:
return
sem = asyncio.Semaphore(5)
+
async def fetch_with_sem(key, details):
async with sem:
return key, details, await self.fetch_feed(details["id"])
diff --git a/themoviedb/tmdb_utils.py b/themoviedb/tmdb_utils.py
index 31e39dc1..b27087cc 100644
--- a/themoviedb/tmdb_utils.py
+++ b/themoviedb/tmdb_utils.py
@@ -58,6 +58,7 @@
"appletv": {"id": "UC1Myj674wRVXB9I4c6Hm5zA", "name": "Apple TV"},
"amazon": {"id": "UCQJWtTnAHhEG5w4uN0udnUQ", "name": "Amazon Prime Video"},
"mgm": {"id": "UCf5CjDJvsFvtVIhkfmKAwAA", "name": "Metro-Goldwyn-Mayer (MGM)"},
+ "crunchyroll": {"id": "UC6pGDc4bFGD1_36IKv3FnYg", "name": "Crunchyroll"},
}
From b3b982719c89d7e2560c8354261062e1e7585e9e Mon Sep 17 00:00:00 2001
From: MAX <63972751+ltzmax@users.noreply.github.com>
Date: Tue, 19 Aug 2025 18:40:22 +0200
Subject: [PATCH 22/31] Update tmdb_utils.py
---
themoviedb/tmdb_utils.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/themoviedb/tmdb_utils.py b/themoviedb/tmdb_utils.py
index b27087cc..0dd07b76 100644
--- a/themoviedb/tmdb_utils.py
+++ b/themoviedb/tmdb_utils.py
@@ -58,7 +58,7 @@
"appletv": {"id": "UC1Myj674wRVXB9I4c6Hm5zA", "name": "Apple TV"},
"amazon": {"id": "UCQJWtTnAHhEG5w4uN0udnUQ", "name": "Amazon Prime Video"},
"mgm": {"id": "UCf5CjDJvsFvtVIhkfmKAwAA", "name": "Metro-Goldwyn-Mayer (MGM)"},
- "crunchyroll": {"id": "UC6pGDc4bFGD1_36IKv3FnYg", "name": "Crunchyroll"},
+ "crunchyroll": {"id": "UC6pGDc4bFGD1_36IKv3FnYg", "name": "Crunchyroll (Anime, Manga, and More)"},
}
From d744d8c94dcede228fed6074185bb78d85c0a7fd Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Tue, 19 Aug 2025 16:40:32 +0000
Subject: [PATCH 23/31] [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---
themoviedb/tmdb_utils.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/themoviedb/tmdb_utils.py b/themoviedb/tmdb_utils.py
index 0dd07b76..73f9649c 100644
--- a/themoviedb/tmdb_utils.py
+++ b/themoviedb/tmdb_utils.py
@@ -58,7 +58,10 @@
"appletv": {"id": "UC1Myj674wRVXB9I4c6Hm5zA", "name": "Apple TV"},
"amazon": {"id": "UCQJWtTnAHhEG5w4uN0udnUQ", "name": "Amazon Prime Video"},
"mgm": {"id": "UCf5CjDJvsFvtVIhkfmKAwAA", "name": "Metro-Goldwyn-Mayer (MGM)"},
- "crunchyroll": {"id": "UC6pGDc4bFGD1_36IKv3FnYg", "name": "Crunchyroll (Anime, Manga, and More)"},
+ "crunchyroll": {
+ "id": "UC6pGDc4bFGD1_36IKv3FnYg",
+ "name": "Crunchyroll (Anime, Manga, and More)",
+ },
}
From 216cf8f76f9680d400a6e30f5f01f01c16bb751d Mon Sep 17 00:00:00 2001
From: MAX <63972751+ltzmax@users.noreply.github.com>
Date: Tue, 19 Aug 2025 18:40:47 +0200
Subject: [PATCH 24/31] Update tmdb_utils.py
---
themoviedb/tmdb_utils.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/themoviedb/tmdb_utils.py b/themoviedb/tmdb_utils.py
index 0dd07b76..73f9649c 100644
--- a/themoviedb/tmdb_utils.py
+++ b/themoviedb/tmdb_utils.py
@@ -58,7 +58,10 @@
"appletv": {"id": "UC1Myj674wRVXB9I4c6Hm5zA", "name": "Apple TV"},
"amazon": {"id": "UCQJWtTnAHhEG5w4uN0udnUQ", "name": "Amazon Prime Video"},
"mgm": {"id": "UCf5CjDJvsFvtVIhkfmKAwAA", "name": "Metro-Goldwyn-Mayer (MGM)"},
- "crunchyroll": {"id": "UC6pGDc4bFGD1_36IKv3FnYg", "name": "Crunchyroll (Anime, Manga, and More)"},
+ "crunchyroll": {
+ "id": "UC6pGDc4bFGD1_36IKv3FnYg",
+ "name": "Crunchyroll (Anime, Manga, and More)",
+ },
}
From 5b9ebbb76c4215275042f8c56542fd8b866ebf98 Mon Sep 17 00:00:00 2001
From: MAX <63972751+ltzmax@users.noreply.github.com>
Date: Tue, 19 Aug 2025 18:45:09 +0200
Subject: [PATCH 25/31] grammar it.
---
themoviedb/themoviedb.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/themoviedb/themoviedb.py b/themoviedb/themoviedb.py
index 9e612635..ba06bb62 100644
--- a/themoviedb/themoviedb.py
+++ b/themoviedb/themoviedb.py
@@ -303,7 +303,7 @@ async def toggle_channel(self, ctx: commands.Context, *channel_names: str) -> No
or specify multiple studio names to toggle them at once.
**NOTE**:
- Videos may contain more than just a trailer from a movie or tv show, such as behind the scenes content or interviews. This is intended to keep you updated on new content from your favorite studios and channels.
+ Videos may include more than just trailers from movies or TV shows, they can also feature behind-the-scenes content or interviews. This is intended to keep you updated on new content from your favorite studios and channels.
**Examples**:
- `[p]tmdbset toggle marvel`
From 578671f09f51d916860f253520b5e92185ccaf47 Mon Sep 17 00:00:00 2001
From: MAX <63972751+ltzmax@users.noreply.github.com>
Date: Wed, 20 Aug 2025 12:17:41 +0200
Subject: [PATCH 26/31] remove logging for yt shorts being ignored.
---
themoviedb/themoviedb.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/themoviedb/themoviedb.py b/themoviedb/themoviedb.py
index ba06bb62..8d5b2ded 100644
--- a/themoviedb/themoviedb.py
+++ b/themoviedb/themoviedb.py
@@ -203,7 +203,6 @@ async def fetch_with_sem(key, details):
video_url = video_url_elem.attrib["href"]
# Skip YouTube Shorts
if "/shorts/" in video_url:
- logger.info(f"Skipping YouTube Short for {details['name']}: {video_url}")
continue
updates[key]["last_published_ts"] = published_ts
From e426286e13b2c4561ab06790ae918ad995bb870b Mon Sep 17 00:00:00 2001
From: MAX <63972751+ltzmax@users.noreply.github.com>
Date: Wed, 20 Aug 2025 12:25:22 +0200
Subject: [PATCH 27/31] perms.
---
themoviedb/themoviedb.py | 15 +++++++++++----
1 file changed, 11 insertions(+), 4 deletions(-)
diff --git a/themoviedb/themoviedb.py b/themoviedb/themoviedb.py
index 8d5b2ded..52605d75 100644
--- a/themoviedb/themoviedb.py
+++ b/themoviedb/themoviedb.py
@@ -246,10 +246,17 @@ async def check_for_new_trailers(self) -> None:
guild = self.bot.get_guild(guild_id)
if not guild:
continue
- try:
- await self.check_trailers(guild)
- except discord.HTTPException as e:
- logger.error(f"Error checking video for guild {guild.id}: {e}", exc_info=True)
+ channel = guild.get_channel(all_guilds[guild_id].get("notification_channel"))
+ if (
+ not channel.permissions_for(guild.me).send_messages
+ or not channel.permissions_for(guild.me).embed_links
+ ):
+ logger.warning(
+ f"Bot does not have permission to send messages or embed links in the notification channel {notification_channel_id} for guild {guild.name}."
+ )
+ continue
+
+ await self.check_trailers(guild)
@check_for_new_trailers.before_loop
async def before_check_for_new_trailers(self) -> None:
From bf6d7504de93ebcf615b9ccccd9c413d3bb14558 Mon Sep 17 00:00:00 2001
From: MAX <63972751+ltzmax@users.noreply.github.com>
Date: Wed, 20 Aug 2025 12:26:42 +0200
Subject: [PATCH 28/31] fix wrong here.
---
themoviedb/themoviedb.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/themoviedb/themoviedb.py b/themoviedb/themoviedb.py
index 52605d75..e6fb7635 100644
--- a/themoviedb/themoviedb.py
+++ b/themoviedb/themoviedb.py
@@ -252,7 +252,7 @@ async def check_for_new_trailers(self) -> None:
or not channel.permissions_for(guild.me).embed_links
):
logger.warning(
- f"Bot does not have permission to send messages or embed links in the notification channel {notification_channel_id} for guild {guild.name}."
+ f"Bot does not have permission to send messages or embed links in the notification channel {channel.name} in guild {guild.name} (ID: {guild.id}"
)
continue
From 4303a20f37158d91be015f5976b2f49151444e1f Mon Sep 17 00:00:00 2001
From: MAX <63972751+ltzmax@users.noreply.github.com>
Date: Fri, 22 Aug 2025 13:34:01 +0200
Subject: [PATCH 29/31] Update info.json
---
themoviedb/info.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/themoviedb/info.json b/themoviedb/info.json
index cde6af4d..fc05e92e 100644
--- a/themoviedb/info.json
+++ b/themoviedb/info.json
@@ -7,7 +7,7 @@
"description": "Search for informations of movies and TV shows from themoviedb.org.",
"short": "Search for informations of movies and TV shows from themoviedb.org.",
"hidden": false,
- "min_bot_version": "3.5.14",
+ "min_bot_version": "3.5.21",
"tags": [
"movies",
"movie",
From cbd429081401239dd7ce26b28677ec0ac8782049 Mon Sep 17 00:00:00 2001
From: MAX <63972751+ltzmax@users.noreply.github.com>
Date: Fri, 22 Aug 2025 14:11:46 +0200
Subject: [PATCH 30/31] Update info.json
---
themoviedb/info.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/themoviedb/info.json b/themoviedb/info.json
index fc05e92e..678c12e9 100644
--- a/themoviedb/info.json
+++ b/themoviedb/info.json
@@ -3,7 +3,7 @@
"max"
],
"name": "TheMovieDB",
- "install_msg": "Thanks for installing.\nYou will need to set your API key before using this cog. See `[p]tmdbset creds`.\n for documentation.\nIf you enjoy my work, you can donate at [buymeacoffee]().\n## PLEASE NOTE:\nThis cog is in alpha phase and using Components V2 and is not fully tested or and stable enough for production use. This also require the Components V2 PR from discord.py for this cog to work.",
+ "install_msg": "Thanks for installing.\nYou will need to set your API key before using this cog. See `[p]tmdbset creds`.\n for documentation.\nIf you enjoy my work, you can donate at [buymeacoffee]().",
"description": "Search for informations of movies and TV shows from themoviedb.org.",
"short": "Search for informations of movies and TV shows from themoviedb.org.",
"hidden": false,
From 04f686d98bae1aca85bc7c2c906a0eee1567a237 Mon Sep 17 00:00:00 2001
From: MAX <63972751+ltzmax@users.noreply.github.com>
Date: Sun, 24 Aug 2025 16:30:01 +0200
Subject: [PATCH 31/31] Fix session.
---
themoviedb/tmdb_utils.py | 26 ++++++++++++--------------
1 file changed, 12 insertions(+), 14 deletions(-)
diff --git a/themoviedb/tmdb_utils.py b/themoviedb/tmdb_utils.py
index 73f9649c..4d44db45 100644
--- a/themoviedb/tmdb_utils.py
+++ b/themoviedb/tmdb_utils.py
@@ -273,16 +273,17 @@ async def search_and_display(ctx, query: str, media_type: str):
filtered_results = []
for data in page_results:
+ if isinstance(data, Exception):
+ log.warning(f"Page fetch failed: {data}")
+ continue
if not isinstance(data, dict) or "results" not in data:
continue
filtered_results.extend(filter_media_results(data["results"], query, media_type))
- if not filtered_results:
- return await ctx.send(f"No results found for `{query}`.")
+ if not filtered_results:
+ return await ctx.send(f"No results found for `{query}`.")
- if len(filtered_results) == 1:
- session = aiohttp.ClientSession()
- try:
+ if len(filtered_results) == 1:
data = await get_media_data(ctx, session, filtered_results[0]["id"], media_type)
if not data:
return await ctx.send("Failed to fetch media details.")
@@ -291,21 +292,18 @@ async def search_and_display(ctx, query: str, media_type: str):
ctx, data, filtered_results[0]["id"], 0, filtered_results, item_type=media_type
)
await ctx.send(embed=embed, view=view)
- finally:
- await session.close()
- return
+ return
title = "What would you like to select?\n-# Click on the button to views the media details."
header_text = f"{header(title, 'medium')}"
class MediaPaginator(discord.ui.LayoutView):
- def __init__(self, ctx, filtered_results, media_type, items_per_page=12, session=None):
+ def __init__(self, ctx, filtered_results, media_type, items_per_page=12):
super().__init__(timeout=120)
self.ctx = ctx
self.filtered_results = self._sort_results(filtered_results, media_type)
self.media_type = media_type
- self.session = session or aiohttp.ClientSession()
- self.owns_session = session is None
+ self.session = aiohttp.ClientSession()
self.current_page = 0
self.items_per_page = items_per_page
self.message = None
@@ -333,7 +331,7 @@ def _get_label(self, result, index):
keys = key_map[self.media_type]
title = result.get(keys["title"], "Unknown")
date = result.get(keys["date"], "N/A")[:4]
- popularity = result.get("popularity", 0)
+ popularity = round(result.get("popularity", 0), 1)
return f"{index + 1}. {title} ({date}) ({popularity})"
def _build_page_content(self):
@@ -382,7 +380,7 @@ def _disable_all_buttons(self):
async def _cleanup(self):
"""Clean up resources and update message."""
self._disable_all_buttons()
- if self.owns_session and not self.session.closed:
+ if not self.session.closed:
await self.session.close()
if self.message:
try:
@@ -473,7 +471,7 @@ async def callback(self, interaction: discord.Interaction) -> None:
self.view._update_content()
await interaction.response.defer()
await self.view.message.edit(content=None, view=self.view)
- except Exception as e:
+ except discord.HTTPException as e:
await self._send_error(interaction, "Error navigating, please try again.", e)
paginator = MediaPaginator(ctx, filtered_results, media_type)