Draper 36f494ba63 [Audio] One PR to rule them all, One PR to find them, One PR to bring them all, and in the darkness bind them (all-in-one pr) (#2904)
* 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>
2019-10-10 22:09:01 -04:00

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()