mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 03:08:55 -05:00
* More changes
Signed-off-by: Guy <guyreis96@gmail.com>
* Fixed auto play defaulting to playlist
Signed-off-by: Guy <guyreis96@gmail.com>
* Localtrack fix
Signed-off-by: Guy <guyreis96@gmail.com>
* Updated deps .. since for some reason aiosqlite is not being auto installed for everyone
Signed-off-by: Guy <guyreis96@gmail.com>
* Yupo
Signed-off-by: Guy <guyreis96@gmail.com>
* Fixed a crash in [p]now
Signed-off-by: Guy <guyreis96@gmail.com>
* Fixed crash on playlist save
Signed-off-by: Guy <guyreis96@gmail.com>
* Debugging Commit
Signed-off-by: Guy <guyreis96@gmail.com>
* Yet more prints
Signed-off-by: Guy <guyreis96@gmail.com>
* Even more spammy debug
Signed-off-by: Guy <guyreis96@gmail.com>
* Debugging commit + NEw Dispatches
Signed-off-by: Guy <guyreis96@gmail.com>
* Debugging commit + NEw Dispatches
Signed-off-by: Guy <guyreis96@gmail.com>
* Fixed localpath checks
Signed-off-by: Guy <guyreis96@gmail.com>
* more fixes for Localpaths
Signed-off-by: Guy <guyreis96@gmail.com>
* Spelling mistake on method
Signed-off-by: Guy <guyreis96@gmail.com>
* Fixed Crash on event handler
Signed-off-by: Guy <guyreis96@gmail.com>
* Fixed Crash on local search
Signed-off-by: Guy <guyreis96@gmail.com>
* Reduced fuzzy match percentage threshold for local tracks to account for nested folders
Signed-off-by: Guy <guyreis96@gmail.com>
* Fixed a crash on queue end
Signed-off-by: Guy <guyreis96@gmail.com>
* Sigh ... Removed a duplicate dispatch
Signed-off-by: Guy <guyreis96@gmail.com>
* Sigh i removed this before ...
Signed-off-by: Guy <guyreis96@gmail.com>
* Reorder dispatch signatures so all 3 new dispatch have matching signature
Signed-off-by: Guy <guyreis96@gmail.com>
* Formatting
Signed-off-by: Guy <guyreis96@gmail.com>
* Edited Error Event to support localtracks
Signed-off-by: Guy <guyreis96@gmail.com>
* Fix a Crash on track crash :awesome:
Signed-off-by: Guy <guyreis96@gmail.com>
* Yikes soo much spam
Signed-off-by: Guy <guyreis96@gmail.com>
* Remove spam and improve existance check
Signed-off-by: Guy <guyreis96@gmail.com>
* Repeat and Auto-play are mutually exclusive now
Signed-off-by: Guy <guyreis96@gmail.com>
* DEBUGS for Preda
Signed-off-by: Guy <guyreis96@gmail.com>
* Vimeo tracks can be from both these domains "vimeo.com", "beam.pro"
Signed-off-by: Guy <guyreis96@gmail.com>
* I mean Mixer can be from those 2 domains ....
Signed-off-by: Guy <guyreis96@gmail.com>
* Fixed `search sc` command
Signed-off-by: Guy <guyreis96@gmail.com>
* Run everything though lints.
rename localtracks module to dataclasses
Clear lock on errors
Signed-off-by: Draper <guyreis96@gmail.com>
* Try to speed up long playlist loading
Signed-off-by: Draper <guyreis96@gmail.com>
* Im an idiot
Signed-off-by: Draper <guyreis96@gmail.com>
* Im an idiot
Signed-off-by: Draper <guyreis96@gmail.com>
* Added logging for writes
Signed-off-by: Draper <guyreis96@gmail.com>
* Fix crash on cog reload
Signed-off-by: Draper <guyreis96@gmail.com>
* Fix for runtimewarning ?
Signed-off-by: Draper <guyreis96@gmail.com>
* Fix for Local Track cache
Signed-off-by: Draper <guyreis96@gmail.com>
* Remove broken tracks from queue on exception
Theoretically do not auto play if track stop reason is Stopped or cleanup
Signed-off-by: Draper <guyreis96@gmail.com>
* Previous commit was a fluke ... ignore it
Signed-off-by: Draper <guyreis96@gmail.com>
* Change from cleanup to Replaced
Signed-off-by: Draper <guyreis96@gmail.com>
* Fixed AttributeError: 'Track' object has no attribute 'info'.
[p]skip will only work for autoplay is there a track being played.
Fixed Console spam if query saving failed in the background while reloading bot.
Autoplay now respect [p]stop command
Signed-off-by: Guy <guyreis96@gmail.com>
* Black formatting
Fix Issue with auto play working when there is songs in the queue
Stop notifying queue ended if autoplay is on
Signed-off-by: Guy <guyreis96@gmail.com>
* Fixed a crash on track load timeout
Signed-off-by: Guy <guyreis96@gmail.com>
* [p]playlist start will now show the playlist name in embed body
Improved Logic for handling broken tracks when repeat is on.
Signed-off-by: Draper <guyreis96@gmail.com>
* Enqueue tracks as soon as we have the youtube URL ....
This basically changes how spotify urls are handled
Need to test saving spotify playlist
Need to test loading a spotify playlist from file
Need to test enqueuing a spotify playlist
Signed-off-by: Draper <guyreis96@gmail.com>
* Updated a track whrn enqueuing spotify playlist
Signed-off-by: Draper <guyreis96@gmail.com>
* Debug
Signed-off-by: Draper <guyreis96@gmail.com>
* Debug
Signed-off-by: Draper <guyreis96@gmail.com>
* Debug
Signed-off-by: Draper <guyreis96@gmail.com>
* Debug
Signed-off-by: Draper <guyreis96@gmail.com>
* Debug
Signed-off-by: Draper <guyreis96@gmail.com>
* Debug
Signed-off-by: Draper <guyreis96@gmail.com>
* Debug
Signed-off-by: Draper <guyreis96@gmail.com>
* Debug
Signed-off-by: Draper <guyreis96@gmail.com>
* Debug
Signed-off-by: Draper <guyreis96@gmail.com>
* Debug
Signed-off-by: Draper <guyreis96@gmail.com>
* Revert spotify_enqueue changes
Signed-off-by: Draper <guyreis96@gmail.com>
* Revert spotify_enqueue changes
Signed-off-by: Draper <guyreis96@gmail.com>
* Allow to set Lavalink jar version from Environment vars
Signed-off-by: Draper <guyreis96@gmail.com>
* Allow to set Lavalink jar version from Environment vars
Signed-off-by: Draper <guyreis96@gmail.com>
* Fix for a crash on Equalizer, Merge Spotify_enqueue changes and revert manager changes
Signed-off-by: Draper <guyreis96@gmail.com>
* Break playlist enqueue after 10 consecutive failures
Signed-off-by: Draper <guyreis96@gmail.com>
* Auto DC, is not compatible with Auto Play
Signed-off-by: Draper <guyreis96@gmail.com>
* Make notifier aware of guild its being called for
Signed-off-by: Draper <guyreis96@gmail.com>
* Type checking
Signed-off-by: Draper <guyreis96@gmail.com>
* Remove lock from 2 exits that i didn't before
Signed-off-by: Draper <guyreis96@gmail.com>
* Fixed TypeError: spotify_enqueue() got an unexpected keyword argument 'notify'
Signed-off-by: Guy <guyreis96@gmail.com>
* Reorder toggles to alphabetical order
Signed-off-by: Guy <guyreis96@gmail.com>
* Update Query to handle spotify URIs
Signed-off-by: Guy <guyreis96@gmail.com>
* update database
Signed-off-by: Guy <guyreis96@gmail.com>
* Dont say tracks enqued on invalid link
Make autop lay a mod only setting
Signed-off-by: Draper <guyreis96@gmail.com>
* Dont say tracks enqued on invalid spotify link
Signed-off-by: Draper <guyreis96@gmail.com>
* Set default age to 365 days
Signed-off-by: Draper <guyreis96@gmail.com>
* Allow Audio mods to set auto play playlists.
Save playlists songs to cache when migrating
Signed-off-by: Guy <guyreis96@gmail.com>
* Black formatting
Signed-off-by: Guy <guyreis96@gmail.com>
* [p]eq cooldown is not triggered is player check fails (i.e if nothing is currently playing)
Adding and removing reaction is no longer a blocking action
Signed-off-by: Guy <guyreis96@gmail.com>
* changelog for non blocking reaction handles
Signed-off-by: Guy <guyreis96@gmail.com>
* Show auto dc and auto play settings by default
Signed-off-by: Guy <guyreis96@gmail.com>
* lint is being a bitch
Signed-off-by: Guy <guyreis96@gmail.com>
* lint changes
Signed-off-by: Draper <guyreis96@gmail.com>
* stop caching local tracks
Signed-off-by: Draper <guyreis96@gmail.com>
* List of Lavalink.Tracks natively added to Playlist Objects
Signed-off-by: Draper <guyreis96@gmail.com>
* Fix UX changes and should fix autoplay
Signed-off-by: Draper <guyreis96@gmail.com>
* Fixed Skip x number of tracks
Signed-off-by: Draper <guyreis96@gmail.com>
* Lint changes
Signed-off-by: Draper <guyreis96@gmail.com>
* Remvoe dead code
Signed-off-by: Draper <guyreis96@gmail.com>
* Update playlist embed formatting to reflect Preda's suggestions
Signed-off-by: Draper <guyreis96@gmail.com>
* Update change logs
Signed-off-by: Draper <guyreis96@gmail.com>
* Add `async with ctx.typing():` to queue and to local folder
Signed-off-by: Draper <guyreis96@gmail.com>
* Stop queuing now when queue is empty with [p]queue
Signed-off-by: Draper <guyreis96@gmail.com>
* fix ctx.typing()
Signed-off-by: Draper <guyreis96@gmail.com>
* fix ctx.typing()
Signed-off-by: Draper <guyreis96@gmail.com>
* Part 1
Signed-off-by: Draper <guyreis96@gmail.com>
* Dont check local track author and name if title is Unknown
Signed-off-by: Guy <guyreis96@gmail.com>
* Makes auto play more random
Signed-off-by: Guy <guyreis96@gmail.com>
* Fixes local play
Fixed missing format
Signed-off-by: Guy <guyreis96@gmail.com>
* Query.process_input accept lavalink.Track objects
Signed-off-by: Draper <guyreis96@gmail.com>
* docstrings
Signed-off-by: Draper <guyreis96@gmail.com>
* Add TODO for timestamp support
Signed-off-by: Draper <guyreis96@gmail.com>
* Improve autoplay from cache logic (possibly slightly slower but more efficient overall)
Signed-off-by: Draper <guyreis96@gmail.com>
* Add My Lavalink PR as a dependency
Remember to remove this .... The PR will bump it to 0.3.2
Signed-off-by: Draper <guyreis96@gmail.com>
* Add My Lavalink PR as a dependency
Remember to remove this .... The PR will bump it to 0.3.2
Signed-off-by: Draper <guyreis96@gmail.com>
* Add My Lavalink PR as a dependency
Remember to remove this .... The PR will bump it to 0.3.2
Signed-off-by: Draper <guyreis96@gmail.com>
* Compile all regex at runtime
Signed-off-by: Draper <guyreis96@gmail.com>
* Fixes local play
Fixed missing format
Signed-off-by: Guy <guyreis96@gmail.com>
* Revert Dep error
Signed-off-by: Guy <guyreis96@gmail.com>
* black
Signed-off-by: Guy <guyreis96@gmail.com>
* Fixed attribute error
Signed-off-by: Guy <guyreis96@gmail.com>
* add `self.bot.dispatch("audio_disconnect", ctx.guild)` dispatch when the player is disconnected
Signed-off-by: Guy <guyreis96@gmail.com>
* Removed shuffle lock on skip
Signed-off-by: Guy <guyreis96@gmail.com>
* Better logic for auto seek (timestamps)
Signed-off-by: Guy <guyreis96@gmail.com>
* Better logic for auto seek (timestamps)
Signed-off-by: Guy <guyreis96@gmail.com>
* Fixes timestamps on spotify tracks
Signed-off-by: Guy <guyreis96@gmail.com>
* Add ctx typing to playlist enqueue
Signed-off-by: Guy <guyreis96@gmail.com>
* Fix Deps
Signed-off-by: Guy <guyreis96@gmail.com>
* Black formatting + Using new lavalink methods for shuffling
Signed-off-by: Guy <guyreis96@gmail.com>
* remove ctx.typing from playlist start
Signed-off-by: Guy <guyreis96@gmail.com>
* Fixes typerror when enqueuing spotify playlists
Signed-off-by: Guy <guyreis96@gmail.com>
* Fix keyerror
Signed-off-by: Guy <guyreis96@gmail.com>
* black formatting, + embed for [p]audioset cache as I forgot it before
Signed-off-by: Guy <guyreis96@gmail.com>
* Fix Error on playlist upload
Signed-off-by: Guy <guyreis96@gmail.com>
* Fix Text help for bump
Signed-off-by: Guy <guyreis96@gmail.com>
* Allow track bumping while shuffle is on
Signed-off-by: Guy <guyreis96@gmail.com>
* Edit bump embed to be consistent with other embed
Hyperlink tracks and removed dynamic title
Signed-off-by: Guy <guyreis96@gmail.com>
* Black
Signed-off-by: Guy <guyreis96@gmail.com>
* Errors not printing fix?
Signed-off-by: Guy <guyreis96@gmail.com>
* Errors not printing fix?
Signed-off-by: Guy <guyreis96@gmail.com>
* Track enqueued footer now shows correct track position when shuffle is on
Signed-off-by: Guy <guyreis96@gmail.com>
* Update changelogs
Signed-off-by: Guy <guyreis96@gmail.com>
* Fix is_owner check in audioset settings
Signed-off-by: Guy <guyreis96@gmail.com>
* Changelogs
Signed-off-by: Guy <guyreis96@gmail.com>
* Dont store searches with no results in cache, fix malformated playlist to cache upon settings migration
Signed-off-by: Guy <guyreis96@gmail.com>
* _clear_lock_on_error > Needs to be reviewed to see if it has been done correctly
Signed-off-by: Guy <guyreis96@gmail.com>
* _clear_lock_on_error > Needs to be reviewed to see if it has been done correctly
Signed-off-by: Guy <guyreis96@gmail.com>
* Fix Query search so that it works with absolute paths for localtracks
Signed-off-by: Guy <guyreis96@gmail.com>
* Extra error if lavalink is set to external and the query is a localtrack and nothing is found
Signed-off-by: Guy <guyreis96@gmail.com>
* Black
Signed-off-by: Guy <guyreis96@gmail.com>
* More detailed error message
Signed-off-by: Guy <guyreis96@gmail.com>
* [p]seek and [p]skip can be used by user if they are the song requester while DJ mode is enabled, if votes are disabled. , [p]queue shuffle can be used to shuffle the queue manually. and [p]queue clean self can be used to remove all songs you requested from the queue.
Signed-off-by: Guy <guyreis96@gmail.com>
* black
Signed-off-by: Guy <guyreis96@gmail.com>
* All the fixes + a `should_auto_play` dispatch for the tech savy peeps
Signed-off-by: Guy <guyreis96@gmail.com>
* Spellchecker + Pythonic changes
Signed-off-by: Guy <guyreis96@gmail.com>
* NO spam for logs
Signed-off-by: Guy <guyreis96@gmail.com>
* Pass Current voice channel to `red_audio_should_auto_play` dispatch
Signed-off-by: Guy <guyreis96@gmail.com>
* Black
Signed-off-by: Guy <guyreis96@gmail.com>
* playlist upload also updates cache in the background
Signed-off-by: Guy <guyreis96@gmail.com>
* playlist upload also updates cache in the background
Signed-off-by: Guy <guyreis96@gmail.com>
* Add scope to playlist picker
Signed-off-by: Guy <guyreis96@gmail.com>
* Delete Playlist picker message once something is selected
Signed-off-by: Guy <guyreis96@gmail.com>
* OCD Fix
Signed-off-by: Guy <guyreis96@gmail.com>
* Facepalm
Signed-off-by: Guy <guyreis96@gmail.com>
* Fix a Potential crash
Signed-off-by: Guy <guyreis96@gmail.com>
* Update my stupidity
Signed-off-by: Guy <guyreis96@gmail.com>
* Auto Pause + Skip tracks already in playlist upon playlist append + a command to remove duplicated tracks from playlist
Signed-off-by: Guy <guyreis96@gmail.com>
* Fix DJ mode when Role is deleted - Credits go to Neuro Assassin#4779
Fix an issue where auto play MAY not trigger
Signed-off-by: Guy <guyreis96@gmail.com>
* Change log to Neuro Assassin#4779 fix
Signed-off-by: Guy <guyreis96@gmail.com>
* Black
Signed-off-by: Guy <guyreis96@gmail.com>
* Dont auto pause manual pauses
Signed-off-by: Guy <guyreis96@gmail.com>
* Adds `[p]autoplay` that can be run by mods or higher
Signed-off-by: Guy <guyreis96@gmail.com>
* 🤦
Signed-off-by: Guy <guyreis96@gmail.com>
* 2x 🤦
Signed-off-by: Guy <guyreis96@gmail.com>
* Fixed wrong import
Signed-off-by: Guy <guyreis96@gmail.com>
* Added Autoplay notify
Signed-off-by: Guy <guyreis96@gmail.com>
* Added Autoplay notify
Signed-off-by: Guy <guyreis96@gmail.com>
* Black
Signed-off-by: Guy <guyreis96@gmail.com>
* Store Track object as prev song instead of URI
Signed-off-by: Guy <guyreis96@gmail.com>
* Black why do u hate me
Signed-off-by: Guy <guyreis96@gmail.com>
* Fix command name
Signed-off-by: Guy <guyreis96@gmail.com>
* Fix Autoplay notify
Signed-off-by: Guy <guyreis96@gmail.com>
* Fix missing await and TypeError, Thanks Flame
Signed-off-by: Guy <guyreis96@gmail.com>
* Add a list of tracks to show as a menu
Signed-off-by: Guy <guyreis96@gmail.com>
* adds the `[p]genre` command which uses the Spotify and Youtube API
Signed-off-by: Guy <guyreis96@gmail.com>
* Enqueue Playlists from genre command
Signed-off-by: Guy <guyreis96@gmail.com>
* Pretify `[p]genre`
Signed-off-by: Guy <guyreis96@gmail.com>
* Fix a Typo and correct jukebox charge order
Signed-off-by: Guy <guyreis96@gmail.com>
* Add genre command to error handling
Signed-off-by: Guy <guyreis96@gmail.com>
* Type checking
Signed-off-by: Guy <guyreis96@gmail.com>
* Update naming scheme for `[p]genre`
Signed-off-by: Guy <guyreis96@gmail.com>
* Black why do you hate me
Signed-off-by: Guy <guyreis96@gmail.com>
* Fixed `[p]local start`
Playlist picker auto selects if theres just 1 playlist found
`[p]queue cleanself` added
Signed-off-by: Guy <guyreis96@gmail.com>
* *sigh* back compatibility with old localtrack paths
Signed-off-by: Guy <guyreis96@gmail.com>
* *sigh* back compatibility with old localtrack paths, even more
Signed-off-by: Guy <guyreis96@gmail.com>
* *sigh* back compatibility with old localtrack paths Even more
Signed-off-by: Guy <guyreis96@gmail.com>
* Fixes localtracks in playlist info command
Signed-off-by: Guy <guyreis96@gmail.com>
* Debug Local Strings
Signed-off-by: Guy <guyreis96@gmail.com>
* Debug Local Strings
Signed-off-by: Guy <guyreis96@gmail.com>
* Fixes `[p]playlist info` for local tracks + fixed error in `[p]remove`
Signed-off-by: Guy <guyreis96@gmail.com>
* Black
Signed-off-by: Guy <guyreis96@gmail.com>
* Fixes formatting in `[p]playlist info`
Signed-off-by: Guy <guyreis96@gmail.com>
* Fix an issue with User Scope playlists were not being deleted
Signed-off-by: Guy <guyreis96@gmail.com>
* Typechecking
Signed-off-by: Guy <guyreis96@gmail.com>
* Black
Signed-off-by: Guy <guyreis96@gmail.com>
* Fix the logic of `delegate_autoplay`
Signed-off-by: Guy <guyreis96@gmail.com>
* Fix a Crash on Load due to type hinting
Signed-off-by: Guy <guyreis96@gmail.com>
* Fix a Crash on Load due to type hintingBlack + fix order of `red_audio_should_auto_play`
Signed-off-by: Guy <guyreis96@gmail.com>
* Add `red_audio_initialized` dispatch so that ownership of auto play can be maintained after a reload
Signed-off-by: Guy <guyreis96@gmail.com>
* Check if the current owner is loaded before raising an error
Signed-off-by: Guy <guyreis96@gmail.com>
* Fixes the Existence Check in `delegate_autoplay`
Signed-off-by: Guy <guyreis96@gmail.com>
* Turns `own_autoplay` in a property of Audio and improves `delegate_autoplay` Thanks Sinbad!
Signed-off-by: Guy <guyreis96@gmail.com>
* Fix for Localtracks playlists
Signed-off-by: Guy <guyreis96@gmail.com>
* When disconnecting send `Disconnecting...`
Fix Stop after a skip
Fix UX discrepancy on Playlist IDs
Fixed Exception when theres a track error
Signed-off-by: Guy <guyreis96@gmail.com>
* add `on_red_audio_unload` dispatch
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Fix a crash on track start where `player.current` can be none?
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Missing new line
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Allow `--author` for playlist to be used to filter playlist for an specific author.
Plus a few bugfixes for UX
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Rename `remdupe` to `dedupe`
Make global scope always be referenced as Global
add missing backwards quotes around the Playlist ID for 1 string
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Towncrier entries for dep changes
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Remove track index when shuffle is on
Fix Progress bar for livestreams
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Trigger autoplay on `QUEUE_END` event instead of `TRACK_END`
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Can't reproduce Ians bug but here a safeguard agaisnt it just in case
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Fixes 2 Messages that had the wrong formatting
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* standerdize playlist naming scheme
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Fix `[p]autoplay` message when Notify is enabled
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* y u h8 me black
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Fix an issue with `[p]audioset localpath` where the localtracks folder was incorrect
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Pythonic formatting
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Ugh
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Fix a typo
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Silently try to delete messages + fixes error Ian found with `[p]genre`
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* sigh black
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Add humanize_number usage correctly
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Bump RLL to 0.4.0
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Update changelog entries
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Change `bot.db` to new API's added by #2967
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Additional reformatting
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Remove PyCharm noise + Fixes a few Pycharm warnings
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Rework `index` parsing for youtube urls
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Addess Aika's review
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Fix a potential crash, saves guild ID to playlists to avoid an scheme change in the future
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Add handling for Python installs without sqlite3.
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Address Flame's review
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Fix ma stupidity
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Address Aika's latest review.
1. Update docstring for `[p]playlist rename`.
2. Fix punctuation for playlist matching.
3. `[p]playlist update` now respect playlist management perms
4. Playlist management errors now shows playlist name, id and scope where possible
5. Remove duplicated code and dead code.
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
* Pluralize string
Signed-off-by: guyre <27962761+drapersniper@users.noreply.github.com>
1132 lines
43 KiB
Python
1132 lines
43 KiB
Python
import asyncio
|
|
import base64
|
|
import contextlib
|
|
import datetime
|
|
import json
|
|
import logging
|
|
import os
|
|
import random
|
|
import time
|
|
from collections import namedtuple
|
|
from typing import Callable, Dict, List, Mapping, NoReturn, Optional, Tuple, Union
|
|
|
|
try:
|
|
from sqlite3 import Error as SQLError
|
|
from databases import Database
|
|
|
|
HAS_SQL = True
|
|
except ModuleNotFoundError:
|
|
HAS_SQL = False
|
|
SQLError = ModuleNotFoundError
|
|
Database = None
|
|
|
|
import aiohttp
|
|
import discord
|
|
import lavalink
|
|
from lavalink.rest_api import LoadResult
|
|
|
|
from redbot.core import Config, commands
|
|
from redbot.core.bot import Red
|
|
from redbot.core.i18n import Translator, cog_i18n
|
|
from . import dataclasses
|
|
from .errors import InvalidTableError, SpotifyFetchError, YouTubeApiError
|
|
from .playlists import get_playlist
|
|
from .utils import CacheLevel, Notifier, is_allowed, queue_duration, track_limit
|
|
|
|
log = logging.getLogger("red.audio.cache")
|
|
_ = Translator("Audio", __file__)
|
|
|
|
_DROP_YOUTUBE_TABLE = "DROP TABLE youtube;"
|
|
|
|
_CREATE_YOUTUBE_TABLE = """
|
|
CREATE TABLE IF NOT EXISTS youtube(
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
track_info TEXT,
|
|
youtube_url TEXT,
|
|
last_updated TEXT,
|
|
last_fetched TEXT
|
|
);
|
|
"""
|
|
|
|
_CREATE_UNIQUE_INDEX_YOUTUBE_TABLE = (
|
|
"CREATE UNIQUE INDEX IF NOT EXISTS idx_youtube_url ON youtube (track_info, youtube_url);"
|
|
)
|
|
|
|
_INSERT_YOUTUBE_TABLE = """
|
|
INSERT OR REPLACE INTO
|
|
youtube(track_info, youtube_url, last_updated, last_fetched)
|
|
VALUES (:track_info, :track_url, :last_updated, :last_fetched);
|
|
"""
|
|
_QUERY_YOUTUBE_TABLE = "SELECT * FROM youtube WHERE track_info=:track;"
|
|
_UPDATE_YOUTUBE_TABLE = """UPDATE youtube
|
|
SET last_fetched=:last_fetched
|
|
WHERE track_info=:track;"""
|
|
|
|
_DROP_SPOTIFY_TABLE = "DROP TABLE spotify;"
|
|
|
|
_CREATE_UNIQUE_INDEX_SPOTIFY_TABLE = (
|
|
"CREATE UNIQUE INDEX IF NOT EXISTS idx_spotify_uri ON spotify (id, type, uri);"
|
|
)
|
|
|
|
_CREATE_SPOTIFY_TABLE = """
|
|
CREATE TABLE IF NOT EXISTS spotify(
|
|
id TEXT,
|
|
type TEXT,
|
|
uri TEXT,
|
|
track_name TEXT,
|
|
artist_name TEXT,
|
|
song_url TEXT,
|
|
track_info TEXT,
|
|
last_updated TEXT,
|
|
last_fetched TEXT
|
|
);
|
|
"""
|
|
|
|
_INSERT_SPOTIFY_TABLE = """
|
|
INSERT OR REPLACE INTO
|
|
spotify(id, type, uri, track_name, artist_name,
|
|
song_url, track_info, last_updated, last_fetched)
|
|
VALUES (:id, :type, :uri, :track_name, :artist_name,
|
|
:song_url, :track_info, :last_updated, :last_fetched);
|
|
"""
|
|
_QUERY_SPOTIFY_TABLE = "SELECT * FROM spotify WHERE uri=:uri;"
|
|
_UPDATE_SPOTIFY_TABLE = """UPDATE spotify
|
|
SET last_fetched=:last_fetched
|
|
WHERE uri=:uri;"""
|
|
|
|
_DROP_LAVALINK_TABLE = "DROP TABLE lavalink;"
|
|
|
|
_CREATE_LAVALINK_TABLE = """
|
|
CREATE TABLE IF NOT EXISTS lavalink(
|
|
query TEXT,
|
|
data BLOB,
|
|
last_updated TEXT,
|
|
last_fetched TEXT
|
|
|
|
);
|
|
"""
|
|
|
|
_CREATE_UNIQUE_INDEX_LAVALINK_TABLE = (
|
|
"CREATE UNIQUE INDEX IF NOT EXISTS idx_lavalink_query ON lavalink (query);"
|
|
)
|
|
|
|
_INSERT_LAVALINK_TABLE = """
|
|
INSERT OR REPLACE INTO
|
|
lavalink(query, data, last_updated, last_fetched)
|
|
VALUES (:query, :data, :last_updated, :last_fetched);
|
|
"""
|
|
_QUERY_LAVALINK_TABLE = "SELECT * FROM lavalink WHERE query=:query;"
|
|
_QUERY_LAST_FETCHED_LAVALINK_TABLE = (
|
|
"SELECT * FROM lavalink "
|
|
"WHERE last_fetched LIKE :day1"
|
|
" OR last_fetched LIKE :day2"
|
|
" OR last_fetched LIKE :day3"
|
|
" OR last_fetched LIKE :day4"
|
|
" OR last_fetched LIKE :day5"
|
|
" OR last_fetched LIKE :day6"
|
|
" OR last_fetched LIKE :day7;"
|
|
)
|
|
_UPDATE_LAVALINK_TABLE = """UPDATE lavalink
|
|
SET last_fetched=:last_fetched
|
|
WHERE query=:query;"""
|
|
|
|
_PARSER = {
|
|
"youtube": {
|
|
"insert": _INSERT_YOUTUBE_TABLE,
|
|
"youtube_url": {"query": _QUERY_YOUTUBE_TABLE},
|
|
"update": _UPDATE_YOUTUBE_TABLE,
|
|
},
|
|
"spotify": {
|
|
"insert": _INSERT_SPOTIFY_TABLE,
|
|
"track_info": {"query": _QUERY_SPOTIFY_TABLE},
|
|
"update": _UPDATE_SPOTIFY_TABLE,
|
|
},
|
|
"lavalink": {
|
|
"insert": _INSERT_LAVALINK_TABLE,
|
|
"data": {"query": _QUERY_LAVALINK_TABLE, "played": _QUERY_LAST_FETCHED_LAVALINK_TABLE},
|
|
"update": _UPDATE_LAVALINK_TABLE,
|
|
},
|
|
}
|
|
|
|
_TOP_100_GLOBALS = "https://www.youtube.com/playlist?list=PL4fGSI1pDJn6puJdseH2Rt9sMvt9E2M4i"
|
|
_TOP_100_US = "https://www.youtube.com/playlist?list=PL4fGSI1pDJn5rWitrRWFKdm-ulaFiIyoK"
|
|
|
|
|
|
class SpotifyAPI:
|
|
"""Wrapper for the Spotify API."""
|
|
|
|
def __init__(self, bot: Red, session: aiohttp.ClientSession):
|
|
self.bot = bot
|
|
self.session = session
|
|
self.spotify_token = None
|
|
self.client_id = None
|
|
self.client_secret = None
|
|
|
|
@staticmethod
|
|
async def _check_token(token: dict):
|
|
now = int(time.time())
|
|
return token["expires_at"] - now < 60
|
|
|
|
@staticmethod
|
|
def _make_token_auth(client_id: Optional[str], client_secret: Optional[str]) -> dict:
|
|
if client_id is None:
|
|
client_id = ""
|
|
if client_secret is None:
|
|
client_secret = ""
|
|
|
|
auth_header = base64.b64encode((client_id + ":" + client_secret).encode("ascii"))
|
|
return {"Authorization": "Basic %s" % auth_header.decode("ascii")}
|
|
|
|
async def _make_get(self, url: str, headers: dict = None, params: dict = None) -> dict:
|
|
if params is None:
|
|
params = {}
|
|
async with self.session.request("GET", url, params=params, headers=headers) as r:
|
|
if r.status != 200:
|
|
log.debug(
|
|
"Issue making GET request to {0}: [{1.status}] {2}".format(
|
|
url, r, await r.json()
|
|
)
|
|
)
|
|
return await r.json()
|
|
|
|
async def _get_auth(self) -> NoReturn:
|
|
if self.client_id is None or self.client_secret is None:
|
|
tokens = await self.bot.get_shared_api_tokens("spotify")
|
|
self.client_id = tokens.get("client_id", "")
|
|
self.client_secret = tokens.get("client_secret", "")
|
|
|
|
async def _request_token(self) -> dict:
|
|
await self._get_auth()
|
|
|
|
payload = {"grant_type": "client_credentials"}
|
|
headers = self._make_token_auth(self.client_id, self.client_secret)
|
|
r = await self.post_call(
|
|
"https://accounts.spotify.com/api/token", payload=payload, headers=headers
|
|
)
|
|
return r
|
|
|
|
async def _get_spotify_token(self) -> Optional[str]:
|
|
if self.spotify_token and not await self._check_token(self.spotify_token):
|
|
return self.spotify_token["access_token"]
|
|
token = await self._request_token()
|
|
if token is None:
|
|
log.debug("Requested a token from Spotify, did not end up getting one.")
|
|
try:
|
|
token["expires_at"] = int(time.time()) + token["expires_in"]
|
|
except KeyError:
|
|
return
|
|
self.spotify_token = token
|
|
log.debug("Created a new access token for Spotify: {0}".format(token))
|
|
return self.spotify_token["access_token"]
|
|
|
|
async def post_call(self, url: str, payload: dict, headers: dict = None) -> dict:
|
|
async with self.session.post(url, data=payload, headers=headers) as r:
|
|
if r.status != 200:
|
|
log.debug(
|
|
"Issue making POST request to {0}: [{1.status}] {2}".format(
|
|
url, r, await r.json()
|
|
)
|
|
)
|
|
return await r.json()
|
|
|
|
async def get_call(self, url: str, params: dict) -> dict:
|
|
token = await self._get_spotify_token()
|
|
return await self._make_get(
|
|
url, params=params, headers={"Authorization": "Bearer {0}".format(token)}
|
|
)
|
|
|
|
async def get_categories(self) -> List[Dict[str, str]]:
|
|
url = "https://api.spotify.com/v1/browse/categories"
|
|
params = {}
|
|
result = await self.get_call(url, params=params)
|
|
with contextlib.suppress(KeyError):
|
|
if result["error"]["status"] == 401:
|
|
raise SpotifyFetchError(
|
|
message=(
|
|
"The Spotify API key or client secret has not been set properly. "
|
|
"\nUse `{prefix}audioset spotifyapi` for instructions."
|
|
)
|
|
)
|
|
categories = result.get("categories", {}).get("items", [])
|
|
return [{c["name"]: c["id"]} for c in categories]
|
|
|
|
async def get_playlist_from_category(self, category: str):
|
|
url = f"https://api.spotify.com/v1/browse/categories/{category}/playlists"
|
|
params = {}
|
|
result = await self.get_call(url, params=params)
|
|
playlists = result.get("playlists", {}).get("items", [])
|
|
return [
|
|
{
|
|
"name": c["name"],
|
|
"uri": c["uri"],
|
|
"url": c.get("external_urls", {}).get("spotify"),
|
|
"tracks": c.get("tracks", {}).get("total", "Unknown"),
|
|
}
|
|
for c in playlists
|
|
]
|
|
|
|
|
|
class YouTubeAPI:
|
|
"""Wrapper for the YouTube Data API."""
|
|
|
|
def __init__(self, bot: Red, session: aiohttp.ClientSession):
|
|
self.bot = bot
|
|
self.session = session
|
|
self.api_key = None
|
|
|
|
async def _get_api_key(self,) -> Optional[str]:
|
|
if self.api_key is None:
|
|
tokens = await self.bot.get_shared_api_tokens("youtube")
|
|
self.api_key = tokens.get("api_key", "")
|
|
return self.api_key
|
|
|
|
async def get_call(self, query: str) -> Optional[str]:
|
|
params = {
|
|
"q": query,
|
|
"part": "id",
|
|
"key": await self._get_api_key(),
|
|
"maxResults": 1,
|
|
"type": "video",
|
|
}
|
|
yt_url = "https://www.googleapis.com/youtube/v3/search"
|
|
async with self.session.request("GET", yt_url, params=params) as r:
|
|
if r.status in [400, 404]:
|
|
return None
|
|
elif r.status in [403, 429]:
|
|
if r.reason == "quotaExceeded":
|
|
raise YouTubeApiError("Your YouTube Data API quota has been reached.")
|
|
|
|
return None
|
|
else:
|
|
search_response = await r.json()
|
|
for search_result in search_response.get("items", []):
|
|
if search_result["id"]["kind"] == "youtube#video":
|
|
return f"https://www.youtube.com/watch?v={search_result['id']['videoId']}"
|
|
|
|
|
|
@cog_i18n(_)
|
|
class MusicCache:
|
|
"""
|
|
Handles music queries to the Spotify and Youtube Data API.
|
|
Always tries the Cache first.
|
|
"""
|
|
|
|
def __init__(self, bot: Red, session: aiohttp.ClientSession, path: str):
|
|
self.bot = bot
|
|
self.spotify_api: SpotifyAPI = SpotifyAPI(bot, session)
|
|
self.youtube_api: YouTubeAPI = YouTubeAPI(bot, session)
|
|
self._session: aiohttp.ClientSession = session
|
|
if HAS_SQL:
|
|
self.database: Database = Database(
|
|
f'sqlite:///{os.path.abspath(str(os.path.join(path, "cache.db")))}'
|
|
)
|
|
else:
|
|
self.database = None
|
|
|
|
self._tasks: dict = {}
|
|
self._lock: asyncio.Lock = asyncio.Lock()
|
|
self.config: Optional[Config] = None
|
|
|
|
async def initialize(self, config: Config) -> NoReturn:
|
|
if HAS_SQL:
|
|
await self.database.connect()
|
|
|
|
await self.database.execute(query="PRAGMA temp_store = 2;")
|
|
await self.database.execute(query="PRAGMA journal_mode = wal;")
|
|
await self.database.execute(query="PRAGMA wal_autocheckpoint;")
|
|
await self.database.execute(query="PRAGMA read_uncommitted = 1;")
|
|
|
|
await self.database.execute(query=_CREATE_LAVALINK_TABLE)
|
|
await self.database.execute(query=_CREATE_UNIQUE_INDEX_LAVALINK_TABLE)
|
|
await self.database.execute(query=_CREATE_YOUTUBE_TABLE)
|
|
await self.database.execute(query=_CREATE_UNIQUE_INDEX_YOUTUBE_TABLE)
|
|
await self.database.execute(query=_CREATE_SPOTIFY_TABLE)
|
|
await self.database.execute(query=_CREATE_UNIQUE_INDEX_SPOTIFY_TABLE)
|
|
self.config = config
|
|
|
|
async def close(self) -> NoReturn:
|
|
if HAS_SQL:
|
|
await self.database.execute(query="PRAGMA optimize;")
|
|
await self.database.disconnect()
|
|
|
|
async def insert(self, table: str, values: List[dict]) -> NoReturn:
|
|
# if table == "spotify":
|
|
# return
|
|
if HAS_SQL:
|
|
query = _PARSER.get(table, {}).get("insert")
|
|
if query is None:
|
|
raise InvalidTableError(f"{table} is not a valid table in the database.")
|
|
|
|
await self.database.execute_many(query=query, values=values)
|
|
|
|
async def update(self, table: str, values: Dict[str, str]) -> NoReturn:
|
|
# if table == "spotify":
|
|
# return
|
|
if HAS_SQL:
|
|
table = _PARSER.get(table, {})
|
|
sql_query = table.get("update")
|
|
time_now = str(datetime.datetime.now(datetime.timezone.utc))
|
|
values["last_fetched"] = time_now
|
|
if not table:
|
|
raise InvalidTableError(f"{table} is not a valid table in the database.")
|
|
await self.database.fetch_one(query=sql_query, values=values)
|
|
|
|
async def fetch_one(
|
|
self, table: str, query: str, values: Dict[str, str]
|
|
) -> Tuple[Optional[str], bool]:
|
|
table = _PARSER.get(table, {})
|
|
sql_query = table.get(query, {}).get("query")
|
|
if HAS_SQL:
|
|
if not table:
|
|
raise InvalidTableError(f"{table} is not a valid table in the database.")
|
|
|
|
row = await self.database.fetch_one(query=sql_query, values=values)
|
|
last_updated = getattr(row, "last_updated", None)
|
|
need_update = True
|
|
with contextlib.suppress(TypeError):
|
|
if last_updated:
|
|
last_update = datetime.datetime.fromisoformat(
|
|
last_updated
|
|
) + datetime.timedelta(days=await self.config.cache_age())
|
|
last_update.replace(tzinfo=datetime.timezone.utc)
|
|
|
|
need_update = last_update < datetime.datetime.now(datetime.timezone.utc)
|
|
|
|
return getattr(row, query, None), need_update if table != "spotify" else True
|
|
else:
|
|
return None, True
|
|
|
|
# TODO: Create a task to remove entries
|
|
# from DB that haven't been fetched in x days ... customizable by Owner
|
|
|
|
async def fetch_all(self, table: str, query: str, values: Dict[str, str]) -> List[Mapping]:
|
|
if HAS_SQL:
|
|
table = _PARSER.get(table, {})
|
|
sql_query = table.get(query, {}).get("played")
|
|
if not table:
|
|
raise InvalidTableError(f"{table} is not a valid table in the database.")
|
|
|
|
return await self.database.fetch_all(query=sql_query, values=values)
|
|
return []
|
|
|
|
@staticmethod
|
|
def _spotify_format_call(qtype: str, key: str) -> Tuple[str, dict]:
|
|
params = {}
|
|
if qtype == "album":
|
|
query = "https://api.spotify.com/v1/albums/{0}/tracks".format(key)
|
|
elif qtype == "track":
|
|
query = "https://api.spotify.com/v1/tracks/{0}".format(key)
|
|
else:
|
|
query = "https://api.spotify.com/v1/playlists/{0}/tracks".format(key)
|
|
return query, params
|
|
|
|
@staticmethod
|
|
def _get_spotify_track_info(track_data: dict) -> Tuple[str, ...]:
|
|
artist_name = track_data["artists"][0]["name"]
|
|
track_name = track_data["name"]
|
|
track_info = f"{track_name} {artist_name}"
|
|
song_url = track_data.get("external_urls", {}).get("spotify")
|
|
uri = track_data["uri"]
|
|
_id = track_data["id"]
|
|
_type = track_data["type"]
|
|
|
|
return song_url, track_info, uri, artist_name, track_name, _id, _type
|
|
|
|
async def _spotify_first_time_query(
|
|
self,
|
|
ctx: commands.Context,
|
|
query_type: str,
|
|
uri: str,
|
|
notifier: Notifier,
|
|
skip_youtube: bool = False,
|
|
current_cache_level: CacheLevel = CacheLevel.none(),
|
|
) -> List[str]:
|
|
youtube_urls = []
|
|
|
|
tracks = await self._spotify_fetch_tracks(query_type, uri, params=None, notifier=notifier)
|
|
total_tracks = len(tracks)
|
|
database_entries = []
|
|
track_count = 0
|
|
time_now = str(datetime.datetime.now(datetime.timezone.utc))
|
|
youtube_cache = CacheLevel.set_youtube().is_subset(current_cache_level)
|
|
for track in tracks:
|
|
if track.get("error", {}).get("message") == "invalid id":
|
|
continue
|
|
(
|
|
song_url,
|
|
track_info,
|
|
uri,
|
|
artist_name,
|
|
track_name,
|
|
_id,
|
|
_type,
|
|
) = self._get_spotify_track_info(track)
|
|
|
|
database_entries.append(
|
|
{
|
|
"id": _id,
|
|
"type": _type,
|
|
"uri": uri,
|
|
"track_name": track_name,
|
|
"artist_name": artist_name,
|
|
"song_url": song_url,
|
|
"track_info": track_info,
|
|
"last_updated": time_now,
|
|
"last_fetched": time_now,
|
|
}
|
|
)
|
|
if skip_youtube is False:
|
|
val = None
|
|
if youtube_cache:
|
|
update = True
|
|
with contextlib.suppress(SQLError):
|
|
val, update = await self.fetch_one(
|
|
"youtube", "youtube_url", {"track": track_info}
|
|
)
|
|
if update:
|
|
val = None
|
|
if val is None:
|
|
val = await self._youtube_first_time_query(
|
|
ctx, track_info, current_cache_level=current_cache_level
|
|
)
|
|
if youtube_cache and val:
|
|
task = ("update", ("youtube", {"track": track_info}))
|
|
self.append_task(ctx, *task)
|
|
|
|
if val:
|
|
youtube_urls.append(val)
|
|
else:
|
|
youtube_urls.append(track_info)
|
|
track_count += 1
|
|
if notifier and ((track_count % 2 == 0) or (track_count == total_tracks)):
|
|
await notifier.notify_user(current=track_count, total=total_tracks, key="youtube")
|
|
if CacheLevel.set_spotify().is_subset(current_cache_level):
|
|
task = ("insert", ("spotify", database_entries))
|
|
self.append_task(ctx, *task)
|
|
return youtube_urls
|
|
|
|
async def _youtube_first_time_query(
|
|
self,
|
|
ctx: commands.Context,
|
|
track_info: str,
|
|
current_cache_level: CacheLevel = CacheLevel.none(),
|
|
) -> str:
|
|
track_url = await self.youtube_api.get_call(track_info)
|
|
if CacheLevel.set_youtube().is_subset(current_cache_level) and track_url:
|
|
time_now = str(datetime.datetime.now(datetime.timezone.utc))
|
|
task = (
|
|
"insert",
|
|
(
|
|
"youtube",
|
|
[
|
|
{
|
|
"track_info": track_info,
|
|
"track_url": track_url,
|
|
"last_updated": time_now,
|
|
"last_fetched": time_now,
|
|
}
|
|
],
|
|
),
|
|
)
|
|
self.append_task(ctx, *task)
|
|
return track_url
|
|
|
|
async def _spotify_fetch_tracks(
|
|
self,
|
|
query_type: str,
|
|
uri: str,
|
|
recursive: Union[str, bool] = False,
|
|
params=None,
|
|
notifier: Optional[Notifier] = None,
|
|
) -> Union[dict, List[str]]:
|
|
|
|
if recursive is False:
|
|
call, params = self._spotify_format_call(query_type, uri)
|
|
results = await self.spotify_api.get_call(call, params)
|
|
else:
|
|
results = await self.spotify_api.get_call(recursive, params)
|
|
try:
|
|
if results["error"]["status"] == 401 and not recursive:
|
|
raise SpotifyFetchError(
|
|
(
|
|
"The Spotify API key or client secret has not been set properly. "
|
|
"\nUse `{prefix}audioset spotifyapi` for instructions."
|
|
)
|
|
)
|
|
elif recursive:
|
|
return {"next": None}
|
|
except KeyError:
|
|
pass
|
|
if recursive:
|
|
return results
|
|
tracks = []
|
|
track_count = 0
|
|
total_tracks = results.get("tracks", results).get("total", 1)
|
|
while True:
|
|
new_tracks = []
|
|
if query_type == "track":
|
|
new_tracks = results
|
|
tracks.append(new_tracks)
|
|
elif query_type == "album":
|
|
tracks_raw = results.get("tracks", results).get("items", [])
|
|
if tracks_raw:
|
|
new_tracks = tracks_raw
|
|
tracks.extend(new_tracks)
|
|
else:
|
|
tracks_raw = results.get("tracks", results).get("items", [])
|
|
if tracks_raw:
|
|
new_tracks = [k["track"] for k in tracks_raw if k.get("track")]
|
|
tracks.extend(new_tracks)
|
|
track_count += len(new_tracks)
|
|
if notifier:
|
|
await notifier.notify_user(current=track_count, total=total_tracks, key="spotify")
|
|
|
|
try:
|
|
if results.get("next") is not None:
|
|
results = await self._spotify_fetch_tracks(
|
|
query_type, uri, results["next"], params, notifier=notifier
|
|
)
|
|
continue
|
|
else:
|
|
break
|
|
except KeyError:
|
|
raise SpotifyFetchError(
|
|
"This doesn't seem to be a valid Spotify playlist/album URL or code."
|
|
)
|
|
|
|
return tracks
|
|
|
|
async def spotify_query(
|
|
self,
|
|
ctx: commands.Context,
|
|
query_type: str,
|
|
uri: str,
|
|
skip_youtube: bool = False,
|
|
notifier: Optional[Notifier] = None,
|
|
) -> List[str]:
|
|
"""
|
|
Queries the Database then falls back to Spotify and YouTube APIs.
|
|
|
|
Parameters
|
|
----------
|
|
ctx: commands.Context
|
|
The context this method is being called under.
|
|
query_type : str
|
|
Type of query to perform (Pl
|
|
uri: str
|
|
Spotify URL ID .
|
|
skip_youtube:bool
|
|
Whether or not to skip YouTube API Calls.
|
|
notifier: Notifier
|
|
A Notifier object to handle the user UI notifications while tracks are loaded.
|
|
Returns
|
|
-------
|
|
List[str]
|
|
List of Youtube URLs.
|
|
"""
|
|
current_cache_level = (
|
|
CacheLevel(await self.config.cache_level()) if HAS_SQL else CacheLevel.none()
|
|
)
|
|
cache_enabled = CacheLevel.set_spotify().is_subset(current_cache_level)
|
|
if query_type == "track" and cache_enabled:
|
|
update = True
|
|
with contextlib.suppress(SQLError):
|
|
val, update = await self.fetch_one(
|
|
"spotify", "track_info", {"uri": f"spotify:track:{uri}"}
|
|
)
|
|
if update:
|
|
val = None
|
|
else:
|
|
val = None
|
|
youtube_urls = []
|
|
if val is None:
|
|
urls = await self._spotify_first_time_query(
|
|
ctx,
|
|
query_type,
|
|
uri,
|
|
notifier,
|
|
skip_youtube,
|
|
current_cache_level=current_cache_level,
|
|
)
|
|
youtube_urls.extend(urls)
|
|
else:
|
|
if query_type == "track" and cache_enabled:
|
|
task = ("update", ("spotify", {"uri": f"spotify:track:{uri}"}))
|
|
self.append_task(ctx, *task)
|
|
youtube_urls.append(val)
|
|
return youtube_urls
|
|
|
|
async def spotify_enqueue(
|
|
self,
|
|
ctx: commands.Context,
|
|
query_type: str,
|
|
uri: str,
|
|
enqueue: bool,
|
|
player: lavalink.Player,
|
|
lock: Callable,
|
|
notifier: Optional[Notifier] = None,
|
|
) -> List[lavalink.Track]:
|
|
track_list = []
|
|
has_not_allowed = False
|
|
try:
|
|
current_cache_level = (
|
|
CacheLevel(await self.config.cache_level()) if HAS_SQL else CacheLevel.none()
|
|
)
|
|
guild_data = await self.config.guild(ctx.guild).all()
|
|
|
|
# now = int(time.time())
|
|
enqueued_tracks = 0
|
|
consecutive_fails = 0
|
|
queue_dur = await queue_duration(ctx)
|
|
queue_total_duration = lavalink.utils.format_time(queue_dur)
|
|
before_queue_length = len(player.queue)
|
|
tracks_from_spotify = await self._spotify_fetch_tracks(
|
|
query_type, uri, params=None, notifier=notifier
|
|
)
|
|
total_tracks = len(tracks_from_spotify)
|
|
if total_tracks < 1:
|
|
lock(ctx, False)
|
|
embed3 = discord.Embed(
|
|
colour=await ctx.embed_colour(),
|
|
title=_("This doesn't seem to be a supported Spotify URL or code."),
|
|
)
|
|
await notifier.update_embed(embed3)
|
|
|
|
return track_list
|
|
database_entries = []
|
|
time_now = str(datetime.datetime.now(datetime.timezone.utc))
|
|
|
|
youtube_cache = CacheLevel.set_youtube().is_subset(current_cache_level)
|
|
spotify_cache = CacheLevel.set_spotify().is_subset(current_cache_level)
|
|
for track_count, track in enumerate(tracks_from_spotify):
|
|
(
|
|
song_url,
|
|
track_info,
|
|
uri,
|
|
artist_name,
|
|
track_name,
|
|
_id,
|
|
_type,
|
|
) = self._get_spotify_track_info(track)
|
|
|
|
database_entries.append(
|
|
{
|
|
"id": _id,
|
|
"type": _type,
|
|
"uri": uri,
|
|
"track_name": track_name,
|
|
"artist_name": artist_name,
|
|
"song_url": song_url,
|
|
"track_info": track_info,
|
|
"last_updated": time_now,
|
|
"last_fetched": time_now,
|
|
}
|
|
)
|
|
val = None
|
|
if youtube_cache:
|
|
update = True
|
|
with contextlib.suppress(SQLError):
|
|
val, update = await self.fetch_one(
|
|
"youtube", "youtube_url", {"track": track_info}
|
|
)
|
|
if update:
|
|
val = None
|
|
if val is None:
|
|
val = await self._youtube_first_time_query(
|
|
ctx, track_info, current_cache_level=current_cache_level
|
|
)
|
|
if youtube_cache and val:
|
|
task = ("update", ("youtube", {"track": track_info}))
|
|
self.append_task(ctx, *task)
|
|
|
|
if val:
|
|
try:
|
|
result, called_api = await self.lavalink_query(
|
|
ctx, player, dataclasses.Query.process_input(val)
|
|
)
|
|
except (RuntimeError, aiohttp.ServerDisconnectedError):
|
|
lock(ctx, False)
|
|
error_embed = discord.Embed(
|
|
colour=await ctx.embed_colour(),
|
|
title=_("The connection was reset while loading the playlist."),
|
|
)
|
|
await notifier.update_embed(error_embed)
|
|
break
|
|
except asyncio.TimeoutError:
|
|
lock(ctx, False)
|
|
error_embed = discord.Embed(
|
|
colour=await ctx.embed_colour(),
|
|
title=_("Player timedout, skipping remaning tracks."),
|
|
)
|
|
await notifier.update_embed(error_embed)
|
|
break
|
|
track_object = result.tracks
|
|
else:
|
|
track_object = []
|
|
if (track_count % 2 == 0) or (track_count == total_tracks):
|
|
key = "lavalink"
|
|
seconds = "???"
|
|
second_key = None
|
|
# if track_count == 2:
|
|
# five_time = int(time.time()) - now
|
|
# if track_count >= 2:
|
|
# remain_tracks = total_tracks - track_count
|
|
# time_remain = (remain_tracks / 2) * five_time
|
|
# if track_count < total_tracks:
|
|
# seconds = dynamic_time(int(time_remain))
|
|
# if track_count == total_tracks:
|
|
# seconds = "0s"
|
|
# second_key = "lavalink_time"
|
|
await notifier.notify_user(
|
|
current=track_count,
|
|
total=total_tracks,
|
|
key=key,
|
|
seconds_key=second_key,
|
|
seconds=seconds,
|
|
)
|
|
|
|
if consecutive_fails >= 10:
|
|
error_embed = discord.Embed(
|
|
colour=await ctx.embed_colour(),
|
|
title=_("Failing to get tracks, skipping remaining."),
|
|
)
|
|
await notifier.update_embed(error_embed)
|
|
break
|
|
if not track_object:
|
|
consecutive_fails += 1
|
|
continue
|
|
consecutive_fails = 0
|
|
single_track = track_object[0]
|
|
if not await is_allowed(
|
|
ctx.guild,
|
|
(
|
|
f"{single_track.title} {single_track.author} {single_track.uri} "
|
|
f"{str(dataclasses.Query.process_input(single_track))}"
|
|
),
|
|
):
|
|
has_not_allowed = True
|
|
log.debug(f"Query is not allowed in {ctx.guild} ({ctx.guild.id})")
|
|
continue
|
|
track_list.append(single_track)
|
|
if enqueue:
|
|
if guild_data["maxlength"] > 0:
|
|
if track_limit(single_track, guild_data["maxlength"]):
|
|
enqueued_tracks += 1
|
|
player.add(ctx.author, single_track)
|
|
self.bot.dispatch(
|
|
"red_audio_track_enqueue",
|
|
player.channel.guild,
|
|
single_track,
|
|
ctx.author,
|
|
)
|
|
else:
|
|
enqueued_tracks += 1
|
|
player.add(ctx.author, single_track)
|
|
self.bot.dispatch(
|
|
"red_audio_track_enqueue",
|
|
player.channel.guild,
|
|
single_track,
|
|
ctx.author,
|
|
)
|
|
|
|
if not player.current:
|
|
await player.play()
|
|
if len(track_list) == 0:
|
|
if not has_not_allowed:
|
|
embed3 = discord.Embed(
|
|
colour=await ctx.embed_colour(),
|
|
title=_(
|
|
"Nothing found.\nThe YouTube API key may be invalid "
|
|
"or you may be rate limited on YouTube's search service.\n"
|
|
"Check the YouTube API key again and follow the instructions "
|
|
"at `{prefix}audioset youtubeapi`."
|
|
).format(prefix=ctx.prefix),
|
|
)
|
|
await ctx.send(embed=embed3)
|
|
player.maybe_shuffle()
|
|
if enqueue and tracks_from_spotify:
|
|
if total_tracks > enqueued_tracks:
|
|
maxlength_msg = " {bad_tracks} tracks cannot be queued.".format(
|
|
bad_tracks=(total_tracks - enqueued_tracks)
|
|
)
|
|
else:
|
|
maxlength_msg = ""
|
|
|
|
embed = discord.Embed(
|
|
colour=await ctx.embed_colour(),
|
|
title=_("Playlist Enqueued"),
|
|
description=_("Added {num} tracks to the queue.{maxlength_msg}").format(
|
|
num=enqueued_tracks, maxlength_msg=maxlength_msg
|
|
),
|
|
)
|
|
if not guild_data["shuffle"] and queue_dur > 0:
|
|
embed.set_footer(
|
|
text=_(
|
|
"{time} until start of playlist"
|
|
" playback: starts at #{position} in queue"
|
|
).format(time=queue_total_duration, position=before_queue_length + 1)
|
|
)
|
|
|
|
await notifier.update_embed(embed)
|
|
lock(ctx, False)
|
|
|
|
if spotify_cache:
|
|
task = ("insert", ("spotify", database_entries))
|
|
self.append_task(ctx, *task)
|
|
except Exception as e:
|
|
lock(ctx, False)
|
|
raise e
|
|
finally:
|
|
lock(ctx, False)
|
|
return track_list
|
|
|
|
async def youtube_query(self, ctx: commands.Context, track_info: str) -> str:
|
|
current_cache_level = (
|
|
CacheLevel(await self.config.cache_level()) if HAS_SQL else CacheLevel.none()
|
|
)
|
|
cache_enabled = CacheLevel.set_youtube().is_subset(current_cache_level)
|
|
val = None
|
|
if cache_enabled:
|
|
update = True
|
|
with contextlib.suppress(SQLError):
|
|
val, update = await self.fetch_one("youtube", "youtube_url", {"track": track_info})
|
|
if update:
|
|
val = None
|
|
if val is None:
|
|
youtube_url = await self._youtube_first_time_query(
|
|
ctx, track_info, current_cache_level=current_cache_level
|
|
)
|
|
else:
|
|
if cache_enabled:
|
|
task = ("update", ("youtube", {"track": track_info}))
|
|
self.append_task(ctx, *task)
|
|
youtube_url = val
|
|
return youtube_url
|
|
|
|
async def lavalink_query(
|
|
self,
|
|
ctx: commands.Context,
|
|
player: lavalink.Player,
|
|
query: dataclasses.Query,
|
|
forced: bool = False,
|
|
) -> Tuple[LoadResult, bool]:
|
|
"""
|
|
A replacement for :code:`lavalink.Player.load_tracks`.
|
|
This will try to get a valid cached entry first if not found or if in valid
|
|
it will then call the lavalink API.
|
|
|
|
Parameters
|
|
----------
|
|
ctx: commands.Context
|
|
The context this method is being called under.
|
|
player : lavalink.Player
|
|
The player who's requesting the query.
|
|
query: dataclasses.Query
|
|
The Query object for the query in question.
|
|
forced:bool
|
|
Whether or not to skip cache and call API first..
|
|
Returns
|
|
-------
|
|
Tuple[lavalink.LoadResult, bool]
|
|
Tuple with the Load result and whether or not the API was called.
|
|
"""
|
|
current_cache_level = (
|
|
CacheLevel(await self.config.cache_level()) if HAS_SQL else CacheLevel.none()
|
|
)
|
|
cache_enabled = CacheLevel.set_lavalink().is_subset(current_cache_level)
|
|
val = None
|
|
_raw_query = dataclasses.Query.process_input(query)
|
|
query = str(_raw_query)
|
|
if cache_enabled and not forced and not _raw_query.is_local:
|
|
update = True
|
|
with contextlib.suppress(SQLError):
|
|
val, update = await self.fetch_one("lavalink", "data", {"query": query})
|
|
if update:
|
|
val = None
|
|
if val:
|
|
task = ("update", ("lavalink", {"query": query}))
|
|
self.append_task(ctx, *task)
|
|
if val and not forced:
|
|
data = json.loads(val)
|
|
data["query"] = query
|
|
results = LoadResult(data)
|
|
called_api = False
|
|
if results.has_error:
|
|
# If cached value has an invalid entry make a new call so that it gets updated
|
|
return await self.lavalink_query(ctx, player, _raw_query, forced=True)
|
|
else:
|
|
called_api = True
|
|
results = None
|
|
try:
|
|
results = await player.load_tracks(query)
|
|
except KeyError:
|
|
results = None
|
|
if results is None:
|
|
results = LoadResult({"loadType": "LOAD_FAILED", "playlistInfo": {}, "tracks": []})
|
|
if (
|
|
cache_enabled
|
|
and results.load_type
|
|
and not results.has_error
|
|
and not _raw_query.is_local
|
|
and results.tracks
|
|
):
|
|
with contextlib.suppress(SQLError):
|
|
time_now = str(datetime.datetime.now(datetime.timezone.utc))
|
|
task = (
|
|
"insert",
|
|
(
|
|
"lavalink",
|
|
[
|
|
{
|
|
"query": query,
|
|
"data": json.dumps(results._raw),
|
|
"last_updated": time_now,
|
|
"last_fetched": time_now,
|
|
}
|
|
],
|
|
),
|
|
)
|
|
self.append_task(ctx, *task)
|
|
return results, called_api
|
|
|
|
async def run_tasks(self, ctx: Optional[commands.Context] = None, _id=None):
|
|
lock_id = _id or ctx.message.id
|
|
lock_author = ctx.author if ctx else None
|
|
async with self._lock:
|
|
if lock_id in self._tasks:
|
|
log.debug(f"Running database writes for {lock_id} ({lock_author})")
|
|
with contextlib.suppress(Exception):
|
|
tasks = self._tasks[ctx.message.id]
|
|
del self._tasks[ctx.message.id]
|
|
await asyncio.gather(
|
|
*[asyncio.ensure_future(self.insert(*a)) for a in tasks["insert"]],
|
|
loop=self.bot.loop,
|
|
return_exceptions=True,
|
|
)
|
|
await asyncio.gather(
|
|
*[asyncio.ensure_future(self.update(*a)) for a in tasks["update"]],
|
|
loop=self.bot.loop,
|
|
return_exceptions=True,
|
|
)
|
|
log.debug(f"Completed database writes for {lock_id} " f"({lock_author})")
|
|
|
|
async def run_all_pending_tasks(self):
|
|
async with self._lock:
|
|
log.debug("Running pending writes to database")
|
|
with contextlib.suppress(Exception):
|
|
tasks = {"update": [], "insert": []}
|
|
for k, task in self._tasks.items():
|
|
for t, args in task.items():
|
|
tasks[t].append(args)
|
|
self._tasks = {}
|
|
|
|
await asyncio.gather(
|
|
*[asyncio.ensure_future(self.insert(*a)) for a in tasks["insert"]],
|
|
loop=self.bot.loop,
|
|
return_exceptions=True,
|
|
)
|
|
await asyncio.gather(
|
|
*[asyncio.ensure_future(self.update(*a)) for a in tasks["update"]],
|
|
loop=self.bot.loop,
|
|
return_exceptions=True,
|
|
)
|
|
log.debug("Completed pending writes to database have finished")
|
|
|
|
def append_task(self, ctx: commands.Context, event: str, task: tuple, _id=None):
|
|
lock_id = _id or ctx.message.id
|
|
if lock_id not in self._tasks:
|
|
self._tasks[lock_id] = {"update": [], "insert": []}
|
|
self._tasks[lock_id][event].append(task)
|
|
|
|
async def play_random(self):
|
|
tracks = []
|
|
try:
|
|
query_data = {}
|
|
for i in range(1, 8):
|
|
date = (
|
|
"%"
|
|
+ str(
|
|
(
|
|
datetime.datetime.now(datetime.timezone.utc)
|
|
- datetime.timedelta(days=i)
|
|
).date()
|
|
)
|
|
+ "%"
|
|
)
|
|
query_data[f"day{i}"] = date
|
|
|
|
vals = await self.fetch_all("lavalink", "data", query_data)
|
|
recently_played = [r.data for r in vals if r]
|
|
|
|
if recently_played:
|
|
track = random.choice(recently_played)
|
|
results = LoadResult(json.loads(track))
|
|
tracks = list(results.tracks)
|
|
except Exception:
|
|
tracks = []
|
|
|
|
return tracks
|
|
|
|
async def autoplay(self, player: lavalink.Player):
|
|
autoplaylist = await self.config.guild(player.channel.guild).autoplaylist()
|
|
current_cache_level = (
|
|
CacheLevel(await self.config.cache_level()) if HAS_SQL else CacheLevel.none()
|
|
)
|
|
cache_enabled = CacheLevel.set_lavalink().is_subset(current_cache_level)
|
|
playlist = None
|
|
tracks = None
|
|
if autoplaylist["enabled"]:
|
|
with contextlib.suppress(Exception):
|
|
playlist = await get_playlist(
|
|
autoplaylist["id"],
|
|
autoplaylist["scope"],
|
|
self.bot,
|
|
player.channel.guild,
|
|
player.channel.guild.me,
|
|
)
|
|
tracks = playlist.tracks_obj
|
|
|
|
if not tracks or not getattr(playlist, "tracks", None):
|
|
if cache_enabled:
|
|
tracks = await self.play_random()
|
|
if not tracks:
|
|
ctx = namedtuple("Context", "message")
|
|
results, called_api = await self.lavalink_query(
|
|
ctx(player.channel.guild), player, dataclasses.Query.process_input(_TOP_100_US)
|
|
)
|
|
tracks = list(results.tracks)
|
|
if tracks:
|
|
multiple = len(tracks) > 1
|
|
track = tracks[0]
|
|
|
|
valid = not multiple
|
|
|
|
while valid is False and multiple:
|
|
track = random.choice(tracks)
|
|
query = dataclasses.Query.process_input(track)
|
|
if not query.valid:
|
|
continue
|
|
if query.is_local and not query.track.exists():
|
|
continue
|
|
if not await is_allowed(
|
|
player.channel.guild,
|
|
(
|
|
f"{track.title} {track.author} {track.uri} "
|
|
f"{str(dataclasses.Query.process_input(track))}"
|
|
),
|
|
):
|
|
log.debug(
|
|
"Query is not allowed in "
|
|
f"{player.channel.guild} ({player.channel.guild.id})"
|
|
)
|
|
continue
|
|
valid = True
|
|
|
|
track.extras = {"autoplay": True}
|
|
player.add(player.channel.guild.me, track)
|
|
self.bot.dispatch(
|
|
"red_audio_track_auto_play", player.channel.guild, track, player.channel.guild.me
|
|
)
|
|
if not player.current:
|
|
await player.play()
|