async def upsert( self, scope: str, playlist_id: int, playlist_name: str, scope_id: int, author_id: int, playlist_url: Optional[str], tracks: List[MutableMapping], ): """Insert or update a playlist into the database.""" scope_type = self.get_scope_type(scope) with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: executor.submit( self.database.cursor().execute, self.statement.upsert, { "scope_type": str(scope_type), "playlist_id": int(playlist_id), "playlist_name": str(playlist_name), "scope_id": int(scope_id), "author_id": int(author_id), "playlist_url": playlist_url, "tracks": json.dumps(tracks), }, )
async def enqueued(self, guild_id: int, room_id: int, track: lavalink.Track): enqueue_time = track.extras.get("enqueue_time", 0) if enqueue_time == 0: track.extras["enqueue_time"] = int(time.time()) track_identifier = track.track_identifier track = self.cog.track_to_json(track) with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: executor.submit( self.database.cursor().execute, PERSIST_QUEUE_UPSERT, { "guild_id": int(guild_id), "room_id": int(room_id), "played": False, "time": enqueue_time, "track": json.dumps(track), "track_id": track_identifier, }, )
async def _load_v3_playlist( self, ctx: commands.Context, scope: str, uploaded_playlist_name: str, uploaded_playlist_url: str, track_list: List, author: Union[discord.User, discord.Member], guild: Union[discord.Guild], ) -> None: embed1 = discord.Embed(title=_("Please wait, adding tracks...")) playlist_msg = await self.send_embed_msg(ctx, embed=embed1) track_count = len(track_list) uploaded_track_count = len(track_list) await asyncio.sleep(1) embed2 = discord.Embed( colour=await ctx.embed_colour(), title=_("Loading track {num}/{total}...").format( num=track_count, total=uploaded_track_count), ) await playlist_msg.edit(embed=embed2) playlist = await create_playlist( ctx, self.playlist_api, scope, uploaded_playlist_name, uploaded_playlist_url, track_list, author, guild, ) scope_name = self.humanize_scope( scope, ctx=guild if scope == PlaylistScope.GUILD.value else author) if not track_count: msg = _("Empty playlist {name} (`{id}`) [**{scope}**] created." ).format(name=playlist.name, id=playlist.id, scope=scope_name) elif uploaded_track_count != track_count: bad_tracks = uploaded_track_count - track_count msg = _( "Added {num} tracks from the {playlist_name} playlist. {num_bad} track(s) " "could not be loaded.").format(num=track_count, playlist_name=playlist.name, num_bad=bad_tracks) else: msg = _("Added {num} tracks from the {playlist_name} playlist." ).format(num=track_count, playlist_name=playlist.name) embed3 = discord.Embed(colour=await ctx.embed_colour(), title=_("Playlist Saved"), description=msg) await playlist_msg.edit(embed=embed3) database_entries = [] time_now = int( datetime.datetime.now(datetime.timezone.utc).timestamp()) async for t in AsyncIter(track_list): uri = t.get("info", {}).get("uri") if uri: t = {"loadType": "V2_COMPAT", "tracks": [t], "query": uri} data = json.dumps(t) if all(k in data for k in ["loadType", "playlistInfo", "isSeekable", "isStream"]): database_entries.append({ "query": uri, "data": data, "last_updated": time_now, "last_fetched": time_now, }) if database_entries: await self.api_interface.local_cache_api.lavalink.insert( database_entries)
async def fetch_track( self, ctx: commands.Context, player: lavalink.Player, query: Query, forced: bool = False, lazy: bool = False, should_query_global: bool = True, ) -> 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: audio_dataclasses.Query The Query object for the query in question. forced:bool Whether or not to skip cache and call API first. lazy:bool If set to True, it will not call the api if a track is not found. should_query_global:bool If the method should query the global database. 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()) cache_enabled = CacheLevel.set_lavalink().is_subset( current_cache_level) val = None query = Query.process_input(query, self.cog.local_folder_current_path) query_string = str(query) globaldb_toggle = self.cog.global_api_user.get("can_read") valid_global_entry = False results = None called_api = False prefer_lyrics = await self.cog.get_lyrics_status(ctx) if prefer_lyrics and query.is_youtube and query.is_search: query_string = f"{query} - lyrics" if cache_enabled and not forced and not query.is_local: try: (val, last_updated) = await self.local_cache_api.lavalink.fetch_one( {"query": query_string}) except Exception as exc: debug_exc_log( log, exc, f"Failed to fetch '{query_string}' from Lavalink table") if val and isinstance(val, dict): if IS_DEBUG: log.debug(f"Updating Local Database with {query_string}") task = ("update", ("lavalink", {"query": query_string})) self.append_task(ctx, *task) else: val = None if val and not forced and isinstance(val, dict): valid_global_entry = False called_api = False else: val = None if (globaldb_toggle and not val and should_query_global and not forced and not query.is_local and not query.is_spotify): valid_global_entry = False with contextlib.suppress(Exception): global_entry = await self.global_cache_api.get_call(query=query ) if global_entry.get("loadType") == "V2_COMPACT": global_entry["loadType"] = "V2_COMPAT" results = LoadResult(global_entry) if results.load_type in [ LoadType.PLAYLIST_LOADED, LoadType.TRACK_LOADED, LoadType.SEARCH_RESULT, LoadType.V2_COMPAT, ]: valid_global_entry = True if valid_global_entry: if IS_DEBUG: log.debug(f"Querying Global DB api for {query}") results, called_api = results, False if valid_global_entry: pass elif lazy is True: called_api = False elif val and not forced and isinstance(val, dict): data = val data["query"] = query_string if data.get("loadType") == "V2_COMPACT": data["loadType"] = "V2_COMPAT" 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 results, called_api = await self.fetch_track(ctx, player, query, forced=True) valid_global_entry = False else: if IS_DEBUG: log.debug(f"Querying Lavalink api for {query_string}") called_api = True try: results = await player.load_tracks(query_string) except KeyError: results = None except RuntimeError: raise TrackEnqueueError if results is None: results = LoadResult({ "loadType": "LOAD_FAILED", "playlistInfo": {}, "tracks": [] }) valid_global_entry = False update_global = (globaldb_toggle and not valid_global_entry and self.global_cache_api.has_api_key) with contextlib.suppress(Exception): if (update_global and not query.is_local and not results.has_error and len(results.tracks) >= 1): global_task = ("global", dict(llresponse=results, query=query)) self.append_task(ctx, *global_task) if (cache_enabled and results.load_type and not results.has_error and not query.is_local and results.tracks): try: time_now = int( datetime.datetime.now(datetime.timezone.utc).timestamp()) data = json.dumps(results._raw) if all(k in data for k in ["loadType", "playlistInfo", "isSeekable", "isStream"]): task = ( "insert", ( "lavalink", [{ "query": query_string, "data": data, "last_updated": time_now, "last_fetched": time_now, }], ), ) self.append_task(ctx, *task) except Exception as exc: debug_exc_log( log, exc, f"Failed to enqueue write task for '{query_string}' to Lavalink table", ) return results, called_api
async def data_schema_migration(self, from_version: int, to_version: int) -> None: database_entries = [] time_now = int( datetime.datetime.now(datetime.timezone.utc).timestamp()) if from_version == to_version: return if from_version < 2 <= to_version: all_guild_data = await self.config.all_guilds() all_playlist = {} async for guild_id, guild_data in AsyncIter( all_guild_data.items()): temp_guild_playlist = guild_data.pop("playlists", None) if temp_guild_playlist: guild_playlist = {} async for count, (name, data) in AsyncIter( temp_guild_playlist.items()).enumerate(start=1000): if not data or not name: continue playlist = { "id": count, "name": name, "guild": int(guild_id) } playlist.update(data) guild_playlist[str(count)] = playlist tracks_in_playlist = data.get("tracks", []) or [] async for t in AsyncIter(tracks_in_playlist): uri = t.get("info", {}).get("uri") if uri: t = { "loadType": "V2_COMPAT", "tracks": [t], "query": uri, } data = json.dumps(t) if all(k in data for k in [ "loadType", "playlistInfo", "isSeekable", "isStream", ]): database_entries.append({ "query": uri, "data": data, "last_updated": time_now, "last_fetched": time_now, }) if guild_playlist: all_playlist[str(guild_id)] = guild_playlist await self.config.custom(PlaylistScope.GUILD.value ).set(all_playlist) # new schema is now in place await self.config.schema_version.set(2) # migration done, now let's delete all the old stuff async for guild_id in AsyncIter(all_guild_data): await self.config.guild( cast(discord.Guild, discord.Object(id=guild_id))).clear_raw("playlists") if from_version < 3 <= to_version: for scope in PlaylistScope.list(): scope_playlist = await get_all_playlist_for_migration23( self.bot, self.playlist_api, self.config, scope) async for p in AsyncIter(scope_playlist): await p.save() await self.config.custom(scope).clear() await self.config.schema_version.set(3) if database_entries: await self.api_interface.local_cache_api.lavalink.insert( database_entries)