[pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci
This commit is contained in:
pre-commit-ci[bot] 2025-12-28 07:58:12 +00:00
parent 0e7473a807
commit e5dd3aec86
7 changed files with 340 additions and 370 deletions

View File

@ -367,21 +367,21 @@ class Music(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.history = pomice.TrackHistory(max_size=100) self.history = pomice.TrackHistory(max_size=100)
@commands.command() @commands.command()
async def stats(self, ctx): async def stats(self, ctx):
"""Show queue statistics.""" """Show queue statistics."""
player = ctx.voice_client player = ctx.voice_client
stats = pomice.QueueStats(player.queue) stats = pomice.QueueStats(player.queue)
summary = stats.get_summary() summary = stats.get_summary()
await ctx.send( await ctx.send(
f"**Queue Stats**\n" f"**Queue Stats**\n"
f"Tracks: {summary['total_tracks']}\n" f"Tracks: {summary['total_tracks']}\n"
f"Duration: {summary['total_duration_formatted']}\n" f"Duration: {summary['total_duration_formatted']}\n"
f"Streams: {summary['stream_count']}" f"Streams: {summary['stream_count']}"
) )
@commands.command() @commands.command()
async def export(self, ctx): async def export(self, ctx):
"""Export queue to file.""" """Export queue to file."""
@ -392,18 +392,18 @@ class Music(commands.Cog):
name=f"{ctx.guild.name}'s Queue" name=f"{ctx.guild.name}'s Queue"
) )
await ctx.send('✅ Queue exported!') await ctx.send('✅ Queue exported!')
@commands.command() @commands.command()
async def filter_long(self, ctx): async def filter_long(self, ctx):
"""Show tracks longer than 5 minutes.""" """Show tracks longer than 5 minutes."""
player = ctx.voice_client player = ctx.voice_client
tracks = list(player.queue) tracks = list(player.queue)
long_tracks = pomice.TrackFilter.by_duration( long_tracks = pomice.TrackFilter.by_duration(
tracks, tracks,
min_duration=300000 # 5 minutes min_duration=300000 # 5 minutes
) )
await ctx.send(f'Found {len(long_tracks)} long tracks!') await ctx.send(f'Found {len(long_tracks)} long tracks!')
``` ```

View File

@ -126,18 +126,18 @@ class Music(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.history = pomice.TrackHistory(max_size=100) self.history = pomice.TrackHistory(max_size=100)
@commands.Cog.listener() @commands.Cog.listener()
async def on_pomice_track_end(self, player, track, _): async def on_pomice_track_end(self, player, track, _):
# Add to history when track ends # Add to history when track ends
self.history.add(track) self.history.add(track)
@commands.command() @commands.command()
async def stats(self, ctx): async def stats(self, ctx):
"""Show queue statistics.""" """Show queue statistics."""
stats = pomice.QueueStats(ctx.voice_client.queue) stats = pomice.QueueStats(ctx.voice_client.queue)
summary = stats.get_summary() summary = stats.get_summary()
await ctx.send( await ctx.send(
f"**Queue Stats**\n" f"**Queue Stats**\n"
f"📊 Tracks: {summary['total_tracks']}\n" f"📊 Tracks: {summary['total_tracks']}\n"
@ -145,19 +145,19 @@ class Music(commands.Cog):
f"📡 Streams: {summary['stream_count']}\n" f"📡 Streams: {summary['stream_count']}\n"
f"👥 Unique Requesters: {summary['unique_requesters']}" f"👥 Unique Requesters: {summary['unique_requesters']}"
) )
@commands.command() @commands.command()
async def history(self, ctx, limit: int = 10): async def history(self, ctx, limit: int = 10):
"""Show recently played tracks.""" """Show recently played tracks."""
recent = self.history.get_last(limit) recent = self.history.get_last(limit)
tracks_list = '\n'.join( tracks_list = '\n'.join(
f"{i}. {track.title} by {track.author}" f"{i}. {track.title} by {track.author}"
for i, track in enumerate(recent, 1) for i, track in enumerate(recent, 1)
) )
await ctx.send(f"**Recently Played:**\n{tracks_list}") await ctx.send(f"**Recently Played:**\n{tracks_list}")
@commands.command() @commands.command()
async def export(self, ctx): async def export(self, ctx):
"""Export current queue.""" """Export current queue."""

View File

@ -7,14 +7,15 @@ This example demonstrates:
- Playlist Export/Import - Playlist Export/Import
- Track Filtering and Search - Track Filtering and Search
""" """
import asyncio import asyncio
import discord import discord
from discord.ext import commands from discord.ext import commands
import pomice import pomice
# Initialize bot # Initialize bot
bot = commands.Bot(command_prefix='!', intents=discord.Intents.all()) bot = commands.Bot(command_prefix="!", intents=discord.Intents.all())
class AdvancedMusic(commands.Cog): class AdvancedMusic(commands.Cog):
@ -23,7 +24,7 @@ class AdvancedMusic(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.pomice = pomice.NodePool() self.pomice = pomice.NodePool()
# Track history for each guild # Track history for each guild
self.track_histories = {} self.track_histories = {}
@ -31,10 +32,10 @@ class AdvancedMusic(commands.Cog):
"""Start Lavalink nodes.""" """Start Lavalink nodes."""
await self.pomice.create_node( await self.pomice.create_node(
bot=self.bot, bot=self.bot,
host='127.0.0.1', host="127.0.0.1",
port='3030', port="3030",
password='youshallnotpass', password="youshallnotpass",
identifier='MAIN' identifier="MAIN",
) )
@commands.Cog.listener() @commands.Cog.listener()
@ -42,10 +43,10 @@ class AdvancedMusic(commands.Cog):
"""Add track to history when it ends.""" """Add track to history when it ends."""
if player.guild.id not in self.track_histories: if player.guild.id not in self.track_histories:
self.track_histories[player.guild.id] = pomice.TrackHistory(max_size=100) self.track_histories[player.guild.id] = pomice.TrackHistory(max_size=100)
self.track_histories[player.guild.id].add(track) self.track_histories[player.guild.id].add(track)
@commands.command(name='play') @commands.command(name="play")
async def play(self, ctx, *, search: str): async def play(self, ctx, *, search: str):
"""Play a track.""" """Play a track."""
if not ctx.voice_client: if not ctx.voice_client:
@ -55,133 +56,111 @@ class AdvancedMusic(commands.Cog):
results = await player.get_tracks(query=search, ctx=ctx) results = await player.get_tracks(query=search, ctx=ctx)
if not results: if not results:
return await ctx.send('No results found.') return await ctx.send("No results found.")
if isinstance(results, pomice.Playlist): if isinstance(results, pomice.Playlist):
await player.queue.put(results.tracks) await player.queue.put(results.tracks)
await ctx.send(f'Added playlist **{results.name}** with {len(results.tracks)} tracks.') await ctx.send(f"Added playlist **{results.name}** with {len(results.tracks)} tracks.")
else: else:
track = results[0] track = results[0]
await player.queue.put(track) await player.queue.put(track)
await ctx.send(f'Added **{track.title}** to queue.') await ctx.send(f"Added **{track.title}** to queue.")
if not player.is_playing: if not player.is_playing:
await player.do_next() await player.do_next()
@commands.command(name='history') @commands.command(name="history")
async def history(self, ctx, limit: int = 10): async def history(self, ctx, limit: int = 10):
"""Show recently played tracks.""" """Show recently played tracks."""
if ctx.guild.id not in self.track_histories: if ctx.guild.id not in self.track_histories:
return await ctx.send('No history available.') return await ctx.send("No history available.")
history = self.track_histories[ctx.guild.id] history = self.track_histories[ctx.guild.id]
if history.is_empty: if history.is_empty:
return await ctx.send('No tracks in history.') return await ctx.send("No tracks in history.")
tracks = history.get_last(limit) tracks = history.get_last(limit)
embed = discord.Embed( embed = discord.Embed(title="🎵 Recently Played", color=discord.Color.blue())
title='🎵 Recently Played',
color=discord.Color.blue()
)
for i, track in enumerate(tracks, 1): for i, track in enumerate(tracks, 1):
embed.add_field( embed.add_field(name=f"{i}. {track.title}", value=f"by {track.author}", inline=False)
name=f'{i}. {track.title}',
value=f'by {track.author}',
inline=False
)
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.command(name='stats') @commands.command(name="stats")
async def queue_stats(self, ctx): async def queue_stats(self, ctx):
"""Show detailed queue statistics.""" """Show detailed queue statistics."""
if not ctx.voice_client: if not ctx.voice_client:
return await ctx.send('Not connected to voice.') return await ctx.send("Not connected to voice.")
player = ctx.voice_client player = ctx.voice_client
stats = pomice.QueueStats(player.queue) stats = pomice.QueueStats(player.queue)
summary = stats.get_summary() summary = stats.get_summary()
embed = discord.Embed( embed = discord.Embed(title="📊 Queue Statistics", color=discord.Color.green())
title='📊 Queue Statistics',
color=discord.Color.green() embed.add_field(name="Total Tracks", value=summary["total_tracks"], inline=True)
)
embed.add_field( embed.add_field(
name='Total Tracks', name="Total Duration", value=summary["total_duration_formatted"], inline=True,
value=summary['total_tracks'],
inline=True
) )
embed.add_field( embed.add_field(
name='Total Duration', name="Average Duration", value=summary["average_duration_formatted"], inline=True,
value=summary['total_duration_formatted'],
inline=True
) )
embed.add_field(
name='Average Duration', if summary["longest_track"]:
value=summary['average_duration_formatted'],
inline=True
)
if summary['longest_track']:
embed.add_field( embed.add_field(
name='Longest Track', name="Longest Track",
value=f"{summary['longest_track'].title} ({stats.format_duration(summary['longest_track'].length)})", value=f"{summary['longest_track'].title} ({stats.format_duration(summary['longest_track'].length)})",
inline=False inline=False,
) )
# Duration breakdown # Duration breakdown
breakdown = summary['duration_breakdown'] breakdown = summary["duration_breakdown"]
embed.add_field( embed.add_field(
name='Duration Breakdown', name="Duration Breakdown",
value=f"Short (<3min): {breakdown['short']}\n" value=f"Short (<3min): {breakdown['short']}\n"
f"Medium (3-6min): {breakdown['medium']}\n" f"Medium (3-6min): {breakdown['medium']}\n"
f"Long (6-10min): {breakdown['long']}\n" f"Long (6-10min): {breakdown['long']}\n"
f"Very Long (>10min): {breakdown['very_long']}", f"Very Long (>10min): {breakdown['very_long']}",
inline=False inline=False,
) )
# Top requesters # Top requesters
top_requesters = stats.get_top_requesters(3) top_requesters = stats.get_top_requesters(3)
if top_requesters: if top_requesters:
requesters_text = '\n'.join( requesters_text = "\n".join(
f'{i}. {req.display_name}: {count} tracks' f"{i}. {req.display_name}: {count} tracks"
for i, (req, count) in enumerate(top_requesters, 1) for i, (req, count) in enumerate(top_requesters, 1)
) )
embed.add_field( embed.add_field(name="Top Requesters", value=requesters_text, inline=False)
name='Top Requesters',
value=requesters_text,
inline=False
)
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.command(name='export') @commands.command(name="export")
async def export_queue(self, ctx, filename: str = 'playlist.json'): async def export_queue(self, ctx, filename: str = "playlist.json"):
"""Export current queue to a file.""" """Export current queue to a file."""
if not ctx.voice_client: if not ctx.voice_client:
return await ctx.send('Not connected to voice.') return await ctx.send("Not connected to voice.")
player = ctx.voice_client player = ctx.voice_client
if player.queue.is_empty: if player.queue.is_empty:
return await ctx.send('Queue is empty.') return await ctx.send("Queue is empty.")
try: try:
pomice.PlaylistManager.export_queue( pomice.PlaylistManager.export_queue(
player.queue, player.queue,
f'playlists/{filename}', f"playlists/{filename}",
name=f"{ctx.guild.name}'s Playlist", name=f"{ctx.guild.name}'s Playlist",
description=f'Exported from {ctx.guild.name}' description=f"Exported from {ctx.guild.name}",
) )
await ctx.send(f'✅ Queue exported to `playlists/{filename}`') await ctx.send(f"✅ Queue exported to `playlists/{filename}`")
except Exception as e: except Exception as e:
await ctx.send(f'❌ Error exporting queue: {e}') await ctx.send(f"❌ Error exporting queue: {e}")
@commands.command(name='import') @commands.command(name="import")
async def import_playlist(self, ctx, filename: str): async def import_playlist(self, ctx, filename: str):
"""Import a playlist from a file.""" """Import a playlist from a file."""
if not ctx.voice_client: if not ctx.voice_client:
@ -190,11 +169,11 @@ class AdvancedMusic(commands.Cog):
player = ctx.voice_client player = ctx.voice_client
try: try:
data = pomice.PlaylistManager.import_playlist(f'playlists/{filename}') data = pomice.PlaylistManager.import_playlist(f"playlists/{filename}")
# Get URIs and search for tracks # Get URIs and search for tracks
uris = [track['uri'] for track in data['tracks'] if track.get('uri')] uris = [track["uri"] for track in data["tracks"] if track.get("uri")]
added = 0 added = 0
for uri in uris: for uri in uris:
try: try:
@ -208,76 +187,73 @@ class AdvancedMusic(commands.Cog):
added += 1 added += 1
except: except:
continue continue
await ctx.send(f'✅ Imported {added} tracks from `{data["name"]}`') await ctx.send(f'✅ Imported {added} tracks from `{data["name"]}`')
if not player.is_playing: if not player.is_playing:
await player.do_next() await player.do_next()
except FileNotFoundError:
await ctx.send(f'❌ Playlist file `{filename}` not found.')
except Exception as e:
await ctx.send(f'❌ Error importing playlist: {e}')
@commands.command(name='filter') except FileNotFoundError:
await ctx.send(f"❌ Playlist file `{filename}` not found.")
except Exception as e:
await ctx.send(f"❌ Error importing playlist: {e}")
@commands.command(name="filter")
async def filter_queue(self, ctx, filter_type: str, *, value: str): async def filter_queue(self, ctx, filter_type: str, *, value: str):
"""Filter queue by various criteria. """Filter queue by various criteria.
Examples: Examples:
!filter author Imagine Dragons !filter author Imagine Dragons
!filter duration 180000-300000 (3-5 minutes in ms) !filter duration 180000-300000 (3-5 minutes in ms)
!filter title Thunder !filter title Thunder
""" """
if not ctx.voice_client: if not ctx.voice_client:
return await ctx.send('Not connected to voice.') return await ctx.send("Not connected to voice.")
player = ctx.voice_client player = ctx.voice_client
queue_tracks = list(player.queue) queue_tracks = list(player.queue)
if filter_type == 'author': if filter_type == "author":
filtered = pomice.TrackFilter.by_author(queue_tracks, value) filtered = pomice.TrackFilter.by_author(queue_tracks, value)
elif filter_type == 'title': elif filter_type == "title":
filtered = pomice.TrackFilter.by_title(queue_tracks, value) filtered = pomice.TrackFilter.by_title(queue_tracks, value)
elif filter_type == 'duration': elif filter_type == "duration":
# Parse duration range (e.g., "180000-300000") # Parse duration range (e.g., "180000-300000")
if '-' in value: if "-" in value:
min_dur, max_dur = map(int, value.split('-')) min_dur, max_dur = map(int, value.split("-"))
filtered = pomice.TrackFilter.by_duration( filtered = pomice.TrackFilter.by_duration(
queue_tracks, queue_tracks, min_duration=min_dur, max_duration=max_dur,
min_duration=min_dur,
max_duration=max_dur
) )
else: else:
return await ctx.send('Duration format: min-max (in milliseconds)') return await ctx.send("Duration format: min-max (in milliseconds)")
else: else:
return await ctx.send('Valid filters: author, title, duration') return await ctx.send("Valid filters: author, title, duration")
if not filtered: if not filtered:
return await ctx.send('No tracks match the filter.') return await ctx.send("No tracks match the filter.")
embed = discord.Embed( embed = discord.Embed(
title=f'🔍 Filtered Results ({len(filtered)} tracks)', title=f"🔍 Filtered Results ({len(filtered)} tracks)", color=discord.Color.purple(),
color=discord.Color.purple()
) )
for i, track in enumerate(filtered[:10], 1): for i, track in enumerate(filtered[:10], 1):
stats = pomice.QueueStats(player.queue) stats = pomice.QueueStats(player.queue)
embed.add_field( embed.add_field(
name=f'{i}. {track.title}', name=f"{i}. {track.title}",
value=f'by {track.author} - {stats.format_duration(track.length)}', value=f"by {track.author} - {stats.format_duration(track.length)}",
inline=False inline=False,
) )
if len(filtered) > 10: if len(filtered) > 10:
embed.set_footer(text=f'Showing 10 of {len(filtered)} results') embed.set_footer(text=f"Showing 10 of {len(filtered)} results")
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.command(name='search_history') @commands.command(name="search_history")
async def search_history(self, ctx, *, query: str): async def search_history(self, ctx, *, query: str):
"""Search through play history.""" """Search through play history."""
if ctx.guild.id not in self.track_histories: if ctx.guild.id not in self.track_histories:
return await ctx.send('No history available.') return await ctx.send("No history available.")
history = self.track_histories[ctx.guild.id] history = self.track_histories[ctx.guild.id]
results = history.search(query) results = history.search(query)
@ -287,63 +263,59 @@ class AdvancedMusic(commands.Cog):
embed = discord.Embed( embed = discord.Embed(
title=f'🔍 History Search: "{query}"', title=f'🔍 History Search: "{query}"',
description=f'Found {len(results)} tracks', description=f"Found {len(results)} tracks",
color=discord.Color.gold() color=discord.Color.gold(),
) )
for i, track in enumerate(results[:10], 1): for i, track in enumerate(results[:10], 1):
embed.add_field( embed.add_field(name=f"{i}. {track.title}", value=f"by {track.author}", inline=False)
name=f'{i}. {track.title}',
value=f'by {track.author}',
inline=False
)
if len(results) > 10: if len(results) > 10:
embed.set_footer(text=f'Showing 10 of {len(results)} results') embed.set_footer(text=f"Showing 10 of {len(results)} results")
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.command(name='sort') @commands.command(name="sort")
async def sort_queue(self, ctx, sort_by: str = 'duration'): async def sort_queue(self, ctx, sort_by: str = "duration"):
"""Sort the queue. """Sort the queue.
Options: duration, title, author Options: duration, title, author
""" """
if not ctx.voice_client: if not ctx.voice_client:
return await ctx.send('Not connected to voice.') return await ctx.send("Not connected to voice.")
player = ctx.voice_client player = ctx.voice_client
if player.queue.is_empty: if player.queue.is_empty:
return await ctx.send('Queue is empty.') return await ctx.send("Queue is empty.")
queue_tracks = list(player.queue) queue_tracks = list(player.queue)
if sort_by == 'duration': if sort_by == "duration":
sorted_tracks = pomice.SearchHelper.sort_by_duration(queue_tracks) sorted_tracks = pomice.SearchHelper.sort_by_duration(queue_tracks)
elif sort_by == 'title': elif sort_by == "title":
sorted_tracks = pomice.SearchHelper.sort_by_title(queue_tracks) sorted_tracks = pomice.SearchHelper.sort_by_title(queue_tracks)
elif sort_by == 'author': elif sort_by == "author":
sorted_tracks = pomice.SearchHelper.sort_by_author(queue_tracks) sorted_tracks = pomice.SearchHelper.sort_by_author(queue_tracks)
else: else:
return await ctx.send('Valid options: duration, title, author') return await ctx.send("Valid options: duration, title, author")
# Clear and refill queue # Clear and refill queue
player.queue._queue.clear() player.queue._queue.clear()
for track in sorted_tracks: for track in sorted_tracks:
await player.queue.put(track) await player.queue.put(track)
await ctx.send(f'✅ Queue sorted by {sort_by}') await ctx.send(f"✅ Queue sorted by {sort_by}")
@bot.event @bot.event
async def on_ready(): async def on_ready():
print(f'{bot.user} is ready!') print(f"{bot.user} is ready!")
await bot.get_cog('AdvancedMusic').start_nodes() await bot.get_cog("AdvancedMusic").start_nodes()
# Add cog # Add cog
bot.add_cog(AdvancedMusic(bot)) bot.add_cog(AdvancedMusic(bot))
# Run bot # Run bot
bot.run('YOUR_BOT_TOKEN') bot.run("YOUR_BOT_TOKEN")

View File

@ -13,7 +13,7 @@ __all__ = ("TrackHistory",)
class TrackHistory: class TrackHistory:
"""Track history manager for Pomice. """Track history manager for Pomice.
Keeps track of previously played tracks with a configurable maximum size. Keeps track of previously played tracks with a configurable maximum size.
Useful for implementing 'previous track' functionality and viewing play history. Useful for implementing 'previous track' functionality and viewing play history.
""" """
@ -22,7 +22,7 @@ class TrackHistory:
def __init__(self, max_size: int = 100) -> None: def __init__(self, max_size: int = 100) -> None:
"""Initialize the track history. """Initialize the track history.
Parameters Parameters
---------- ----------
max_size: int max_size: int
@ -46,7 +46,7 @@ class TrackHistory:
def __getitem__(self, index: int) -> Track: def __getitem__(self, index: int) -> Track:
"""Get a track at the given index in history. """Get a track at the given index in history.
Parameters Parameters
---------- ----------
index: int index: int
@ -59,7 +59,7 @@ class TrackHistory:
def add(self, track: Track) -> None: def add(self, track: Track) -> None:
"""Add a track to the history. """Add a track to the history.
Parameters Parameters
---------- ----------
track: Track track: Track
@ -70,12 +70,12 @@ class TrackHistory:
def get_last(self, count: int = 1) -> List[Track]: def get_last(self, count: int = 1) -> List[Track]:
"""Get the last N tracks from history. """Get the last N tracks from history.
Parameters Parameters
---------- ----------
count: int count: int
Number of tracks to retrieve. Defaults to 1. Number of tracks to retrieve. Defaults to 1.
Returns Returns
------- -------
List[Track] List[Track]
@ -87,7 +87,7 @@ class TrackHistory:
def get_previous(self) -> Optional[Track]: def get_previous(self) -> Optional[Track]:
"""Get the previous track in history. """Get the previous track in history.
Returns Returns
------- -------
Optional[Track] Optional[Track]
@ -95,13 +95,13 @@ class TrackHistory:
""" """
if not self._history or self._current_index <= 0: if not self._history or self._current_index <= 0:
return None return None
self._current_index -= 1 self._current_index -= 1
return self._history[self._current_index] return self._history[self._current_index]
def get_next(self) -> Optional[Track]: def get_next(self) -> Optional[Track]:
"""Get the next track in history (when navigating backwards). """Get the next track in history (when navigating backwards).
Returns Returns
------- -------
Optional[Track] Optional[Track]
@ -109,7 +109,7 @@ class TrackHistory:
""" """
if not self._history or self._current_index >= len(self._history) - 1: if not self._history or self._current_index >= len(self._history) - 1:
return None return None
self._current_index += 1 self._current_index += 1
return self._history[self._current_index] return self._history[self._current_index]
@ -120,7 +120,7 @@ class TrackHistory:
def get_all(self) -> List[Track]: def get_all(self) -> List[Track]:
"""Get all tracks in history. """Get all tracks in history.
Returns Returns
------- -------
List[Track] List[Track]
@ -130,12 +130,12 @@ class TrackHistory:
def search(self, query: str) -> List[Track]: def search(self, query: str) -> List[Track]:
"""Search for tracks in history by title or author. """Search for tracks in history by title or author.
Parameters Parameters
---------- ----------
query: str query: str
Search query (case-insensitive) Search query (case-insensitive)
Returns Returns
------- -------
List[Track] List[Track]
@ -143,13 +143,14 @@ class TrackHistory:
""" """
query_lower = query.lower() query_lower = query.lower()
return [ return [
track for track in reversed(self._history) track
for track in reversed(self._history)
if query_lower in track.title.lower() or query_lower in track.author.lower() if query_lower in track.title.lower() or query_lower in track.author.lower()
] ]
def get_unique_tracks(self) -> List[Track]: def get_unique_tracks(self) -> List[Track]:
"""Get unique tracks from history (removes duplicates). """Get unique tracks from history (removes duplicates).
Returns Returns
------- -------
List[Track] List[Track]
@ -165,19 +166,20 @@ class TrackHistory:
def get_by_requester(self, requester_id: int) -> List[Track]: def get_by_requester(self, requester_id: int) -> List[Track]:
"""Get all tracks requested by a specific user. """Get all tracks requested by a specific user.
Parameters Parameters
---------- ----------
requester_id: int requester_id: int
Discord user ID Discord user ID
Returns Returns
------- -------
List[Track] List[Track]
Tracks requested by the user (most recent first) Tracks requested by the user (most recent first)
""" """
return [ return [
track for track in reversed(self._history) track
for track in reversed(self._history)
if track.requester and track.requester.id == requester_id if track.requester and track.requester.id == requester_id
] ]

View File

@ -18,7 +18,7 @@ __all__ = ("PlaylistManager",)
class PlaylistManager: class PlaylistManager:
"""Manager for exporting and importing playlists. """Manager for exporting and importing playlists.
Allows saving queue contents to JSON files and loading them back, Allows saving queue contents to JSON files and loading them back,
useful for persistent playlists and sharing. useful for persistent playlists and sharing.
""" """
@ -33,7 +33,7 @@ class PlaylistManager:
include_metadata: bool = True, include_metadata: bool = True,
) -> None: ) -> None:
"""Export a queue to a JSON file. """Export a queue to a JSON file.
Parameters Parameters
---------- ----------
queue: Queue queue: Queue
@ -48,60 +48,60 @@ class PlaylistManager:
Whether to include requester and timestamp metadata. Defaults to True. Whether to include requester and timestamp metadata. Defaults to True.
""" """
path = Path(filepath) path = Path(filepath)
if name is None: if name is None:
name = path.stem name = path.stem
tracks_data = [] tracks_data = []
for track in queue: for track in queue:
track_dict = { track_dict = {
'title': track.title, "title": track.title,
'author': track.author, "author": track.author,
'uri': track.uri, "uri": track.uri,
'identifier': track.identifier, "identifier": track.identifier,
'length': track.length, "length": track.length,
'is_stream': track.is_stream, "is_stream": track.is_stream,
} }
if include_metadata: if include_metadata:
track_dict['requester_id'] = track.requester.id if track.requester else None track_dict["requester_id"] = track.requester.id if track.requester else None
track_dict['requester_name'] = str(track.requester) if track.requester else None track_dict["requester_name"] = str(track.requester) if track.requester else None
track_dict['timestamp'] = track.timestamp track_dict["timestamp"] = track.timestamp
if track.thumbnail: if track.thumbnail:
track_dict['thumbnail'] = track.thumbnail track_dict["thumbnail"] = track.thumbnail
if track.isrc: if track.isrc:
track_dict['isrc'] = track.isrc track_dict["isrc"] = track.isrc
if track.playlist: if track.playlist:
track_dict['playlist_name'] = track.playlist.name track_dict["playlist_name"] = track.playlist.name
tracks_data.append(track_dict) tracks_data.append(track_dict)
playlist_data = { playlist_data = {
'name': name, "name": name,
'description': description, "description": description,
'created_at': datetime.utcnow().isoformat(), "created_at": datetime.utcnow().isoformat(),
'track_count': len(tracks_data), "track_count": len(tracks_data),
'total_duration': sum(t['length'] for t in tracks_data), "total_duration": sum(t["length"] for t in tracks_data),
'tracks': tracks_data, "tracks": tracks_data,
'version': '1.0', "version": "1.0",
} }
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
with open(path, 'w', encoding='utf-8') as f: with open(path, "w", encoding="utf-8") as f:
json.dump(playlist_data, f, indent=2, ensure_ascii=False) json.dump(playlist_data, f, indent=2, ensure_ascii=False)
@staticmethod @staticmethod
def import_playlist(filepath: str) -> Dict[str, Any]: def import_playlist(filepath: str) -> Dict[str, Any]:
"""Import a playlist from a JSON file. """Import a playlist from a JSON file.
Parameters Parameters
---------- ----------
filepath: str filepath: str
Path to the JSON file Path to the JSON file
Returns Returns
------- -------
Dict[str, Any] Dict[str, Any]
@ -114,10 +114,10 @@ class PlaylistManager:
- 'created_at': Creation timestamp - 'created_at': Creation timestamp
""" """
path = Path(filepath) path = Path(filepath)
with open(path, 'r', encoding='utf-8') as f: with open(path, encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
return data return data
@staticmethod @staticmethod
@ -129,7 +129,7 @@ class PlaylistManager:
description: Optional[str] = None, description: Optional[str] = None,
) -> None: ) -> None:
"""Export a list of tracks to a JSON file. """Export a list of tracks to a JSON file.
Parameters Parameters
---------- ----------
tracks: List[Track] tracks: List[Track]
@ -142,53 +142,53 @@ class PlaylistManager:
Description for the playlist Description for the playlist
""" """
path = Path(filepath) path = Path(filepath)
if name is None: if name is None:
name = path.stem name = path.stem
tracks_data = [ tracks_data = [
{ {
'title': track.title, "title": track.title,
'author': track.author, "author": track.author,
'uri': track.uri, "uri": track.uri,
'identifier': track.identifier, "identifier": track.identifier,
'length': track.length, "length": track.length,
'thumbnail': track.thumbnail, "thumbnail": track.thumbnail,
'isrc': track.isrc, "isrc": track.isrc,
} }
for track in tracks for track in tracks
] ]
playlist_data = { playlist_data = {
'name': name, "name": name,
'description': description, "description": description,
'created_at': datetime.utcnow().isoformat(), "created_at": datetime.utcnow().isoformat(),
'track_count': len(tracks_data), "track_count": len(tracks_data),
'total_duration': sum(t['length'] for t in tracks_data), "total_duration": sum(t["length"] for t in tracks_data),
'tracks': tracks_data, "tracks": tracks_data,
'version': '1.0', "version": "1.0",
} }
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
with open(path, 'w', encoding='utf-8') as f: with open(path, "w", encoding="utf-8") as f:
json.dump(playlist_data, f, indent=2, ensure_ascii=False) json.dump(playlist_data, f, indent=2, ensure_ascii=False)
@staticmethod @staticmethod
def get_track_uris(filepath: str) -> List[str]: def get_track_uris(filepath: str) -> List[str]:
"""Get list of track URIs from a saved playlist. """Get list of track URIs from a saved playlist.
Parameters Parameters
---------- ----------
filepath: str filepath: str
Path to the JSON file Path to the JSON file
Returns Returns
------- -------
List[str] List[str]
List of track URIs List of track URIs
""" """
data = PlaylistManager.import_playlist(filepath) data = PlaylistManager.import_playlist(filepath)
return [track['uri'] for track in data['tracks'] if track.get('uri')] return [track["uri"] for track in data["tracks"] if track.get("uri")]
@staticmethod @staticmethod
def merge_playlists( def merge_playlists(
@ -200,7 +200,7 @@ class PlaylistManager:
remove_duplicates: bool = True, remove_duplicates: bool = True,
) -> None: ) -> None:
"""Merge multiple playlists into one. """Merge multiple playlists into one.
Parameters Parameters
---------- ----------
filepaths: List[str] filepaths: List[str]
@ -216,34 +216,34 @@ class PlaylistManager:
""" """
all_tracks = [] all_tracks = []
seen_uris = set() seen_uris = set()
for filepath in filepaths: for filepath in filepaths:
data = PlaylistManager.import_playlist(filepath) data = PlaylistManager.import_playlist(filepath)
for track in data['tracks']: for track in data["tracks"]:
uri = track.get('uri', '') uri = track.get("uri", "")
if remove_duplicates: if remove_duplicates:
if uri and uri in seen_uris: if uri and uri in seen_uris:
continue continue
if uri: if uri:
seen_uris.add(uri) seen_uris.add(uri)
all_tracks.append(track) all_tracks.append(track)
merged_data = { merged_data = {
'name': name or 'Merged Playlist', "name": name or "Merged Playlist",
'description': description or f'Merged from {len(filepaths)} playlists', "description": description or f"Merged from {len(filepaths)} playlists",
'created_at': datetime.utcnow().isoformat(), "created_at": datetime.utcnow().isoformat(),
'track_count': len(all_tracks), "track_count": len(all_tracks),
'total_duration': sum(t['length'] for t in all_tracks), "total_duration": sum(t["length"] for t in all_tracks),
'tracks': all_tracks, "tracks": all_tracks,
'version': '1.0', "version": "1.0",
} }
output = Path(output_path) output = Path(output_path)
output.parent.mkdir(parents=True, exist_ok=True) output.parent.mkdir(parents=True, exist_ok=True)
with open(output, 'w', encoding='utf-8') as f: with open(output, "w", encoding="utf-8") as f:
json.dump(merged_data, f, indent=2, ensure_ascii=False) json.dump(merged_data, f, indent=2, ensure_ascii=False)
@staticmethod @staticmethod
@ -254,7 +254,7 @@ class PlaylistManager:
name: Optional[str] = None, name: Optional[str] = None,
) -> None: ) -> None:
"""Export tracks to M3U playlist format. """Export tracks to M3U playlist format.
Parameters Parameters
---------- ----------
tracks: List[Track] tracks: List[Track]
@ -266,39 +266,39 @@ class PlaylistManager:
""" """
path = Path(filepath) path = Path(filepath)
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
with open(path, 'w', encoding='utf-8') as f: with open(path, "w", encoding="utf-8") as f:
f.write('#EXTM3U\n') f.write("#EXTM3U\n")
if name: if name:
f.write(f'#PLAYLIST:{name}\n') f.write(f"#PLAYLIST:{name}\n")
for track in tracks: for track in tracks:
# Duration in seconds # Duration in seconds
duration = track.length // 1000 duration = track.length // 1000
f.write(f'#EXTINF:{duration},{track.author} - {track.title}\n') f.write(f"#EXTINF:{duration},{track.author} - {track.title}\n")
f.write(f'{track.uri}\n') f.write(f"{track.uri}\n")
@staticmethod @staticmethod
def get_playlist_info(filepath: str) -> Dict[str, Any]: def get_playlist_info(filepath: str) -> Dict[str, Any]:
"""Get basic information about a saved playlist without loading all tracks. """Get basic information about a saved playlist without loading all tracks.
Parameters Parameters
---------- ----------
filepath: str filepath: str
Path to the JSON file Path to the JSON file
Returns Returns
------- -------
Dict[str, Any] Dict[str, Any]
Dictionary with playlist metadata (name, track_count, duration, etc.) Dictionary with playlist metadata (name, track_count, duration, etc.)
""" """
data = PlaylistManager.import_playlist(filepath) data = PlaylistManager.import_playlist(filepath)
return { return {
'name': data.get('name'), "name": data.get("name"),
'description': data.get('description'), "description": data.get("description"),
'track_count': data.get('track_count'), "track_count": data.get("track_count"),
'total_duration': data.get('total_duration'), "total_duration": data.get("total_duration"),
'created_at': data.get('created_at'), "created_at": data.get("created_at"),
'version': data.get('version'), "version": data.get("version"),
} }

View File

@ -15,14 +15,14 @@ __all__ = ("QueueStats",)
class QueueStats: class QueueStats:
"""Advanced statistics for a Pomice Queue. """Advanced statistics for a Pomice Queue.
Provides detailed analytics about queue contents including duration, Provides detailed analytics about queue contents including duration,
requester statistics, and track distribution. requester statistics, and track distribution.
""" """
def __init__(self, queue: Queue) -> None: def __init__(self, queue: Queue) -> None:
"""Initialize queue statistics. """Initialize queue statistics.
Parameters Parameters
---------- ----------
queue: Queue queue: Queue
@ -33,7 +33,7 @@ class QueueStats:
@property @property
def total_duration(self) -> int: def total_duration(self) -> int:
"""Get total duration of all tracks in queue (milliseconds). """Get total duration of all tracks in queue (milliseconds).
Returns Returns
------- -------
int int
@ -44,7 +44,7 @@ class QueueStats:
@property @property
def average_duration(self) -> float: def average_duration(self) -> float:
"""Get average track duration in queue (milliseconds). """Get average track duration in queue (milliseconds).
Returns Returns
------- -------
float float
@ -57,7 +57,7 @@ class QueueStats:
@property @property
def longest_track(self) -> Optional[Track]: def longest_track(self) -> Optional[Track]:
"""Get the longest track in the queue. """Get the longest track in the queue.
Returns Returns
------- -------
Optional[Track] Optional[Track]
@ -70,7 +70,7 @@ class QueueStats:
@property @property
def shortest_track(self) -> Optional[Track]: def shortest_track(self) -> Optional[Track]:
"""Get the shortest track in the queue. """Get the shortest track in the queue.
Returns Returns
------- -------
Optional[Track] Optional[Track]
@ -82,7 +82,7 @@ class QueueStats:
def get_requester_stats(self) -> Dict[int, Dict[str, any]]: def get_requester_stats(self) -> Dict[int, Dict[str, any]]:
"""Get statistics grouped by requester. """Get statistics grouped by requester.
Returns Returns
------- -------
Dict[int, Dict[str, any]] Dict[int, Dict[str, any]]
@ -92,53 +92,51 @@ class QueueStats:
- 'tracks': List of their tracks - 'tracks': List of their tracks
""" """
stats: Dict[int, Dict] = {} stats: Dict[int, Dict] = {}
for track in self._queue: for track in self._queue:
if not track.requester: if not track.requester:
continue continue
user_id = track.requester.id user_id = track.requester.id
if user_id not in stats: if user_id not in stats:
stats[user_id] = { stats[user_id] = {
'count': 0, "count": 0,
'total_duration': 0, "total_duration": 0,
'tracks': [], "tracks": [],
'requester': track.requester "requester": track.requester,
} }
stats[user_id]['count'] += 1 stats[user_id]["count"] += 1
stats[user_id]['total_duration'] += track.length stats[user_id]["total_duration"] += track.length
stats[user_id]['tracks'].append(track) stats[user_id]["tracks"].append(track)
return stats return stats
def get_top_requesters(self, limit: int = 5) -> List[tuple]: def get_top_requesters(self, limit: int = 5) -> List[tuple]:
"""Get top requesters by track count. """Get top requesters by track count.
Parameters Parameters
---------- ----------
limit: int limit: int
Maximum number of requesters to return. Defaults to 5. Maximum number of requesters to return. Defaults to 5.
Returns Returns
------- -------
List[tuple] List[tuple]
List of (requester, count) tuples sorted by count (descending) List of (requester, count) tuples sorted by count (descending)
""" """
requester_counts = Counter( requester_counts = Counter(track.requester.id for track in self._queue if track.requester)
track.requester.id for track in self._queue if track.requester
)
# Get requester objects # Get requester objects
stats = self.get_requester_stats() stats = self.get_requester_stats()
return [ return [
(stats[user_id]['requester'], count) (stats[user_id]["requester"], count)
for user_id, count in requester_counts.most_common(limit) for user_id, count in requester_counts.most_common(limit)
] ]
def get_author_distribution(self) -> Dict[str, int]: def get_author_distribution(self) -> Dict[str, int]:
"""Get distribution of tracks by author. """Get distribution of tracks by author.
Returns Returns
------- -------
Dict[str, int] Dict[str, int]
@ -148,12 +146,12 @@ class QueueStats:
def get_top_authors(self, limit: int = 10) -> List[tuple]: def get_top_authors(self, limit: int = 10) -> List[tuple]:
"""Get most common authors in the queue. """Get most common authors in the queue.
Parameters Parameters
---------- ----------
limit: int limit: int
Maximum number of authors to return. Defaults to 10. Maximum number of authors to return. Defaults to 10.
Returns Returns
------- -------
List[tuple] List[tuple]
@ -164,7 +162,7 @@ class QueueStats:
def get_stream_count(self) -> int: def get_stream_count(self) -> int:
"""Get count of streams in the queue. """Get count of streams in the queue.
Returns Returns
------- -------
int int
@ -174,24 +172,24 @@ class QueueStats:
def get_playlist_distribution(self) -> Dict[str, int]: def get_playlist_distribution(self) -> Dict[str, int]:
"""Get distribution of tracks by playlist. """Get distribution of tracks by playlist.
Returns Returns
------- -------
Dict[str, int] Dict[str, int]
Dictionary mapping playlist names to track counts Dictionary mapping playlist names to track counts
""" """
distribution: Dict[str, int] = {} distribution: Dict[str, int] = {}
for track in self._queue: for track in self._queue:
if track.playlist: if track.playlist:
playlist_name = track.playlist.name playlist_name = track.playlist.name
distribution[playlist_name] = distribution.get(playlist_name, 0) + 1 distribution[playlist_name] = distribution.get(playlist_name, 0) + 1
return distribution return distribution
def get_duration_breakdown(self) -> Dict[str, int]: def get_duration_breakdown(self) -> Dict[str, int]:
"""Get breakdown of tracks by duration categories. """Get breakdown of tracks by duration categories.
Returns Returns
------- -------
Dict[str, int] Dict[str, int]
@ -202,34 +200,34 @@ class QueueStats:
- 'very_long' (> 10 min) - 'very_long' (> 10 min)
""" """
breakdown = { breakdown = {
'short': 0, # < 3 minutes "short": 0, # < 3 minutes
'medium': 0, # 3-6 minutes "medium": 0, # 3-6 minutes
'long': 0, # 6-10 minutes "long": 0, # 6-10 minutes
'very_long': 0 # > 10 minutes "very_long": 0, # > 10 minutes
} }
for track in self._queue: for track in self._queue:
duration_minutes = track.length / 60000 # Convert ms to minutes duration_minutes = track.length / 60000 # Convert ms to minutes
if duration_minutes < 3: if duration_minutes < 3:
breakdown['short'] += 1 breakdown["short"] += 1
elif duration_minutes < 6: elif duration_minutes < 6:
breakdown['medium'] += 1 breakdown["medium"] += 1
elif duration_minutes < 10: elif duration_minutes < 10:
breakdown['long'] += 1 breakdown["long"] += 1
else: else:
breakdown['very_long'] += 1 breakdown["very_long"] += 1
return breakdown return breakdown
def format_duration(self, milliseconds: int) -> str: def format_duration(self, milliseconds: int) -> str:
"""Format duration in milliseconds to human-readable string. """Format duration in milliseconds to human-readable string.
Parameters Parameters
---------- ----------
milliseconds: int milliseconds: int
Duration in milliseconds Duration in milliseconds
Returns Returns
------- -------
str str
@ -238,33 +236,33 @@ class QueueStats:
seconds = milliseconds // 1000 seconds = milliseconds // 1000
minutes, seconds = divmod(seconds, 60) minutes, seconds = divmod(seconds, 60)
hours, minutes = divmod(minutes, 60) hours, minutes = divmod(minutes, 60)
if hours > 0: if hours > 0:
return f"{hours}:{minutes:02d}:{seconds:02d}" return f"{hours}:{minutes:02d}:{seconds:02d}"
return f"{minutes}:{seconds:02d}" return f"{minutes}:{seconds:02d}"
def get_summary(self) -> Dict[str, any]: def get_summary(self) -> Dict[str, any]:
"""Get a comprehensive summary of queue statistics. """Get a comprehensive summary of queue statistics.
Returns Returns
------- -------
Dict[str, any] Dict[str, any]
Dictionary containing various queue statistics Dictionary containing various queue statistics
""" """
return { return {
'total_tracks': len(self._queue), "total_tracks": len(self._queue),
'total_duration': self.total_duration, "total_duration": self.total_duration,
'total_duration_formatted': self.format_duration(self.total_duration), "total_duration_formatted": self.format_duration(self.total_duration),
'average_duration': self.average_duration, "average_duration": self.average_duration,
'average_duration_formatted': self.format_duration(int(self.average_duration)), "average_duration_formatted": self.format_duration(int(self.average_duration)),
'longest_track': self.longest_track, "longest_track": self.longest_track,
'shortest_track': self.shortest_track, "shortest_track": self.shortest_track,
'stream_count': self.get_stream_count(), "stream_count": self.get_stream_count(),
'unique_authors': len(self.get_author_distribution()), "unique_authors": len(self.get_author_distribution()),
'unique_requesters': len(self.get_requester_stats()), "unique_requesters": len(self.get_requester_stats()),
'duration_breakdown': self.get_duration_breakdown(), "duration_breakdown": self.get_duration_breakdown(),
'loop_mode': self._queue.loop_mode, "loop_mode": self._queue.loop_mode,
'is_looping': self._queue.is_looping, "is_looping": self._queue.is_looping,
} }
def __repr__(self) -> str: def __repr__(self) -> str:

View File

@ -13,7 +13,7 @@ __all__ = ("TrackFilter", "SearchHelper")
class TrackFilter: class TrackFilter:
"""Advanced filtering utilities for tracks. """Advanced filtering utilities for tracks.
Provides various filter functions to find tracks matching specific criteria. Provides various filter functions to find tracks matching specific criteria.
""" """
@ -25,7 +25,7 @@ class TrackFilter:
max_duration: Optional[int] = None, max_duration: Optional[int] = None,
) -> List[Track]: ) -> List[Track]:
"""Filter tracks by duration range. """Filter tracks by duration range.
Parameters Parameters
---------- ----------
tracks: List[Track] tracks: List[Track]
@ -34,26 +34,26 @@ class TrackFilter:
Minimum duration in milliseconds Minimum duration in milliseconds
max_duration: Optional[int] max_duration: Optional[int]
Maximum duration in milliseconds Maximum duration in milliseconds
Returns Returns
------- -------
List[Track] List[Track]
Filtered tracks Filtered tracks
""" """
result = tracks result = tracks
if min_duration is not None: if min_duration is not None:
result = [t for t in result if t.length >= min_duration] result = [t for t in result if t.length >= min_duration]
if max_duration is not None: if max_duration is not None:
result = [t for t in result if t.length <= max_duration] result = [t for t in result if t.length <= max_duration]
return result return result
@staticmethod @staticmethod
def by_author(tracks: List[Track], author: str, *, exact: bool = False) -> List[Track]: def by_author(tracks: List[Track], author: str, *, exact: bool = False) -> List[Track]:
"""Filter tracks by author name. """Filter tracks by author name.
Parameters Parameters
---------- ----------
tracks: List[Track] tracks: List[Track]
@ -62,7 +62,7 @@ class TrackFilter:
Author name to search for Author name to search for
exact: bool exact: bool
Whether to match exactly. Defaults to False (case-insensitive contains). Whether to match exactly. Defaults to False (case-insensitive contains).
Returns Returns
------- -------
List[Track] List[Track]
@ -70,14 +70,14 @@ class TrackFilter:
""" """
if exact: if exact:
return [t for t in tracks if t.author == author] return [t for t in tracks if t.author == author]
author_lower = author.lower() author_lower = author.lower()
return [t for t in tracks if author_lower in t.author.lower()] return [t for t in tracks if author_lower in t.author.lower()]
@staticmethod @staticmethod
def by_title(tracks: List[Track], title: str, *, exact: bool = False) -> List[Track]: def by_title(tracks: List[Track], title: str, *, exact: bool = False) -> List[Track]:
"""Filter tracks by title. """Filter tracks by title.
Parameters Parameters
---------- ----------
tracks: List[Track] tracks: List[Track]
@ -86,7 +86,7 @@ class TrackFilter:
Title to search for Title to search for
exact: bool exact: bool
Whether to match exactly. Defaults to False (case-insensitive contains). Whether to match exactly. Defaults to False (case-insensitive contains).
Returns Returns
------- -------
List[Track] List[Track]
@ -94,21 +94,21 @@ class TrackFilter:
""" """
if exact: if exact:
return [t for t in tracks if t.title == title] return [t for t in tracks if t.title == title]
title_lower = title.lower() title_lower = title.lower()
return [t for t in tracks if title_lower in t.title.lower()] return [t for t in tracks if title_lower in t.title.lower()]
@staticmethod @staticmethod
def by_requester(tracks: List[Track], requester_id: int) -> List[Track]: def by_requester(tracks: List[Track], requester_id: int) -> List[Track]:
"""Filter tracks by requester. """Filter tracks by requester.
Parameters Parameters
---------- ----------
tracks: List[Track] tracks: List[Track]
List of tracks to filter List of tracks to filter
requester_id: int requester_id: int
Discord user ID Discord user ID
Returns Returns
------- -------
List[Track] List[Track]
@ -119,34 +119,31 @@ class TrackFilter:
@staticmethod @staticmethod
def by_playlist(tracks: List[Track], playlist_name: str) -> List[Track]: def by_playlist(tracks: List[Track], playlist_name: str) -> List[Track]:
"""Filter tracks by playlist name. """Filter tracks by playlist name.
Parameters Parameters
---------- ----------
tracks: List[Track] tracks: List[Track]
List of tracks to filter List of tracks to filter
playlist_name: str playlist_name: str
Playlist name to search for Playlist name to search for
Returns Returns
------- -------
List[Track] List[Track]
Filtered tracks Filtered tracks
""" """
playlist_lower = playlist_name.lower() playlist_lower = playlist_name.lower()
return [ return [t for t in tracks if t.playlist and playlist_lower in t.playlist.name.lower()]
t for t in tracks
if t.playlist and playlist_lower in t.playlist.name.lower()
]
@staticmethod @staticmethod
def streams_only(tracks: List[Track]) -> List[Track]: def streams_only(tracks: List[Track]) -> List[Track]:
"""Filter to only include streams. """Filter to only include streams.
Parameters Parameters
---------- ----------
tracks: List[Track] tracks: List[Track]
List of tracks to filter List of tracks to filter
Returns Returns
------- -------
List[Track] List[Track]
@ -157,12 +154,12 @@ class TrackFilter:
@staticmethod @staticmethod
def non_streams_only(tracks: List[Track]) -> List[Track]: def non_streams_only(tracks: List[Track]) -> List[Track]:
"""Filter to exclude streams. """Filter to exclude streams.
Parameters Parameters
---------- ----------
tracks: List[Track] tracks: List[Track]
List of tracks to filter List of tracks to filter
Returns Returns
------- -------
List[Track] List[Track]
@ -173,14 +170,14 @@ class TrackFilter:
@staticmethod @staticmethod
def custom(tracks: List[Track], predicate: Callable[[Track], bool]) -> List[Track]: def custom(tracks: List[Track], predicate: Callable[[Track], bool]) -> List[Track]:
"""Filter tracks using a custom predicate function. """Filter tracks using a custom predicate function.
Parameters Parameters
---------- ----------
tracks: List[Track] tracks: List[Track]
List of tracks to filter List of tracks to filter
predicate: Callable[[Track], bool] predicate: Callable[[Track], bool]
Function that returns True for tracks to include Function that returns True for tracks to include
Returns Returns
------- -------
List[Track] List[Track]
@ -202,7 +199,7 @@ class SearchHelper:
case_sensitive: bool = False, case_sensitive: bool = False,
) -> List[Track]: ) -> List[Track]:
"""Search tracks by query string. """Search tracks by query string.
Parameters Parameters
---------- ----------
tracks: List[Track] tracks: List[Track]
@ -215,7 +212,7 @@ class SearchHelper:
Whether to search in authors. Defaults to True. Whether to search in authors. Defaults to True.
case_sensitive: bool case_sensitive: bool
Whether search is case-sensitive. Defaults to False. Whether search is case-sensitive. Defaults to False.
Returns Returns
------- -------
List[Track] List[Track]
@ -223,17 +220,17 @@ class SearchHelper:
""" """
if not case_sensitive: if not case_sensitive:
query = query.lower() query = query.lower()
results = [] results = []
for track in tracks: for track in tracks:
title = track.title if case_sensitive else track.title.lower() title = track.title if case_sensitive else track.title.lower()
author = track.author if case_sensitive else track.author.lower() author = track.author if case_sensitive else track.author.lower()
if search_title and query in title: if search_title and query in title:
results.append(track) results.append(track)
elif search_author and query in author: elif search_author and query in author:
results.append(track) results.append(track)
return results return results
@staticmethod @staticmethod
@ -243,14 +240,14 @@ class SearchHelper:
reverse: bool = False, reverse: bool = False,
) -> List[Track]: ) -> List[Track]:
"""Sort tracks by duration. """Sort tracks by duration.
Parameters Parameters
---------- ----------
tracks: List[Track] tracks: List[Track]
List of tracks to sort List of tracks to sort
reverse: bool reverse: bool
If True, sort longest to shortest. Defaults to False. If True, sort longest to shortest. Defaults to False.
Returns Returns
------- -------
List[Track] List[Track]
@ -265,14 +262,14 @@ class SearchHelper:
reverse: bool = False, reverse: bool = False,
) -> List[Track]: ) -> List[Track]:
"""Sort tracks alphabetically by title. """Sort tracks alphabetically by title.
Parameters Parameters
---------- ----------
tracks: List[Track] tracks: List[Track]
List of tracks to sort List of tracks to sort
reverse: bool reverse: bool
If True, sort Z to A. Defaults to False. If True, sort Z to A. Defaults to False.
Returns Returns
------- -------
List[Track] List[Track]
@ -287,14 +284,14 @@ class SearchHelper:
reverse: bool = False, reverse: bool = False,
) -> List[Track]: ) -> List[Track]:
"""Sort tracks alphabetically by author. """Sort tracks alphabetically by author.
Parameters Parameters
---------- ----------
tracks: List[Track] tracks: List[Track]
List of tracks to sort List of tracks to sort
reverse: bool reverse: bool
If True, sort Z to A. Defaults to False. If True, sort Z to A. Defaults to False.
Returns Returns
------- -------
List[Track] List[Track]
@ -310,7 +307,7 @@ class SearchHelper:
by_title_author: bool = False, by_title_author: bool = False,
) -> List[Track]: ) -> List[Track]:
"""Remove duplicate tracks from a list. """Remove duplicate tracks from a list.
Parameters Parameters
---------- ----------
tracks: List[Track] tracks: List[Track]
@ -319,7 +316,7 @@ class SearchHelper:
Remove duplicates by URI. Defaults to True. Remove duplicates by URI. Defaults to True.
by_title_author: bool by_title_author: bool
Remove duplicates by title+author combination. Defaults to False. Remove duplicates by title+author combination. Defaults to False.
Returns Returns
------- -------
List[Track] List[Track]
@ -327,7 +324,7 @@ class SearchHelper:
""" """
seen = set() seen = set()
result = [] result = []
for track in tracks: for track in tracks:
if by_uri: if by_uri:
key = track.uri key = track.uri
@ -335,22 +332,22 @@ class SearchHelper:
key = (track.title.lower(), track.author.lower()) key = (track.title.lower(), track.author.lower())
else: else:
key = track.track_id key = track.track_id
if key not in seen: if key not in seen:
seen.add(key) seen.add(key)
result.append(track) result.append(track)
return result return result
@staticmethod @staticmethod
def group_by_author(tracks: List[Track]) -> dict[str, List[Track]]: def group_by_author(tracks: List[Track]) -> dict[str, List[Track]]:
"""Group tracks by author. """Group tracks by author.
Parameters Parameters
---------- ----------
tracks: List[Track] tracks: List[Track]
List of tracks to group List of tracks to group
Returns Returns
------- -------
dict[str, List[Track]] dict[str, List[Track]]
@ -367,12 +364,12 @@ class SearchHelper:
@staticmethod @staticmethod
def group_by_playlist(tracks: List[Track]) -> dict[str, List[Track]]: def group_by_playlist(tracks: List[Track]) -> dict[str, List[Track]]:
"""Group tracks by playlist. """Group tracks by playlist.
Parameters Parameters
---------- ----------
tracks: List[Track] tracks: List[Track]
List of tracks to group List of tracks to group
Returns Returns
------- -------
dict[str, List[Track]] dict[str, List[Track]]
@ -390,18 +387,19 @@ class SearchHelper:
@staticmethod @staticmethod
def get_random_tracks(tracks: List[Track], count: int) -> List[Track]: def get_random_tracks(tracks: List[Track], count: int) -> List[Track]:
"""Get random tracks from a list. """Get random tracks from a list.
Parameters Parameters
---------- ----------
tracks: List[Track] tracks: List[Track]
List of tracks List of tracks
count: int count: int
Number of random tracks to get Number of random tracks to get
Returns Returns
------- -------
List[Track] List[Track]
Random tracks (without replacement) Random tracks (without replacement)
""" """
import random import random
return random.sample(tracks, min(count, len(tracks))) return random.sample(tracks, min(count, len(tracks)))