繁体   English   中英

如何从天才那里随机挑选一首歌曲并将一小部分歌词发送到 discord

[英]How to pick a random song from genius and send a small section of those lyrics to discord

@client.command()
async def lyrics(ctx):
    genius = lyricsgenius.Genius(api_key)
    message_ID = ctx.message.author.id
    await ctx.send("**name the song author:**")
    @client.listen("on_message")
    async def on_message(message):
        if message.author.id == int(message_ID):
            artist_name = message.content
            artist = genius.search_artist(artist_name)
            print(artist)
        else:
            return

这是我到目前为止所拥有的。 它要求用户说出艺术家的名字,然后搜索该艺术家在 genius 上的歌曲列表。 然而,这需要很多时间。 我希望它能够随机挑选一首歌曲,然后获取歌词,然后发送一小段歌词。

我已决定将我的评论移至答案。 我已经安装了 package 并稍微玩了一下,您在这里面临的问题是search_artist的实现:确实,它获取了我们想要的完整歌曲数据。 search_songs是一个更合适的选择,另一种选择是重新实现search_artist以便它确实只返回一个结果。 我将只关注获取歌词的逻辑,而不会将其包装到 Discord API 中。

为我调查的起点是公共Genius API 您不能指望以某种方式规避这一点,您正在使用的库只是它的一个包装器(参见例如这个定义)。 没有现成的 API 可以根据请求为给定艺术家获取随机歌曲,对于您的任务,必须至少轮询两个 API 端点。 进一步来说...

首先你做GET /search艺术家查询。 你不能省略这一步——一开始,你有一个艺术家名字,但没有艺术家 ID。 这将返回类似于下面的 JSON 结果(我正在使用Try it!来自上面链接的 Genius API 页面的示例)。

hits: [ { highlights: [...],
index: "song",
type: "song",
result: {
annotation_count: 20,
api_path: "/songs/3039923", (...)

另一种方法是调用search_artists注意末尾的s )。 它可能变得相当丑陋,因为相关信息嵌套在 JSON 中。例如,

genius.search_artists("Led")['sections'][0]['hits'][0]['result']['id']

返回562 ,这是 Led Zeppelin 的艺术家 ID。 这条线看起来很糟糕,但lyricsgenius 的实现与它非常相似,尽管显然有更多的预防措施。 至少,将上面的块包装在try... catch中是有意义的。

如果不需要从搜索返回的顶级艺术家的所有歌曲中随机挑选,更直接的方法是只使用一个裸露的、非艺术家特定的搜索:

songs = genius.search_songs("Led")['hits']

这将导致其他艺术家的一些条目(例如在上述情况下的Lo & Leduc )。

此时,我们有艺术家 ID 和其他信息,但还没有歌曲。 这就是它变得丑陋的地方。 实际上,它已经做到了——Genius API 实际上并没有在一个查询中返回所有歌曲 ID,而是使用了分页。 从设计的角度来看这是明智的,但在您的用例中变通是噩梦般的。 上面的调用返回 10 个条目(默认),通过使用 per_page,每个per_page调用最多可以获得 50 个条目,并且通过提供页面偏移量,您可以浏览单首歌曲。 因此,您可能事先不知道会有多少首歌曲。 问题的另一部分似乎是 Genius 是 SSR-heavy,并且在一次请求大量歌曲时需要很长时间。 这对于数据科学项目来说很好,但对于 discord 机器人来说就不那么好了。

我可能是盲人,但我找不到 function 或 API 中每个艺术家的歌曲总数的字段。 后端可能在内部使用SELECT... FROM... LIMIT offset, count等。 有趣的是,我还发现这种二进制搜索给定艺术家的歌曲数量可能比一次请求大量歌曲更快或大致相同 - 想想

genius.artist_songs(562, per_page=1, page=250)

对比

genius.artist_songs(562, per_page=50, page=5)

上面调用返回的响应将设置一个['next_page']字段(如果存在),并且您还没有到达歌曲列表的末尾。 同样,这不是您可以规避的事情。 现在,一个有趣的问题是优化这种随机搜索以获得最佳的per_page计数,并且如果搜索步骤变得太小,可能会放弃最后几个请求,但是一个好的起点是这样的:

max_page = 1000
page = max_page
next_page = 1
last_known_good_page = 0
last_known_bad_page = max_page
while True:
    next_page = genius.artist_songs(artist_id, per_page=1, page=page)['next_page']
    if next_page:
        last_known_good_page = next_page
        page = int((last_known_bad_page + page) / 2)
    else:
        last_known_bad_page = page
        page = int((last_known_good_page + page) / 2)
    if page - last_known_good_page < 1:
        break

当然,在您的应用程序中集成此类不确定循环时要小心。 例如,如果艺术家有超过max_page个条目,它根本不会终止 对于artist_id = 562 ,它给出了 275 页的正确答案。 上面的代码运行速度与使用per_page = 50max_page = 20一样快。 您可以更多地使用这些参数,如果您依赖每页的多个结果,那么逻辑会变得更加复杂,因为您需要对它们进行计数。

确定歌曲数量后,rest 就很简单了:

random_id = genius.artist_songs(artist_id, per_page=1, page=random.randint(0, page), sort='popularity')['songs'][0]['id']
lyrics = genius.lyrics(random_id)

对于由 Fair Dice Roll™ 确定的random_id = 10 ,您会得到这个漂亮的 output:

"When the Levee Breaks 歌词[Intro]\n\n[Verse 1]\nIf it keeps on raing, levee's going to break\nIf it keeps on raining, the levee's going to break\n\n[Chorus 1]\nWhen堤坝决堤,无处停留\n\n[第 2 节]\n卑鄙的旧堤坝教我哭泣和呻吟,哦\n卑鄙的旧堤坝教我哭泣和呻吟\n[合唱 2]\n它就是这样需要让一个山民离开他的家\n哦好吧,哦好吧,哦好吧,哦哦\n\n[桥 1]\n哦,这不会让你感到难过\n当你试图找到回家的路时\n你不知道该走哪条路\n如果你往南走,他们就没有工作要做\n如果你往北去芝加哥\n啊,啊,啊,嘿\n\n[Instrumental Break] \n\n[第 3 节]\n哭泣对你没有帮助,祈祷对你没有好处\n不,哭泣对你没有帮助,祈祷对你没有好处\n\n[合唱 3] \n当堤坝破裂时,妈妈,你必须移动,哦\n\n[第 4 节]\n昨晚我坐在堤坝上呻吟\n昨晚我坐在堤坝上呻吟\n想着我的孩子和我快乐的家\ nOh-ho\n你可能也喜欢[Bridge 2]\n啊啊啊啊啊啊\n啊啊啊啊啊啊\n哦啊啊\n\n[Outro]\n去\n我去去芝加哥\n去芝加哥\n对不起,但我不能带你去,啊\n下去,现在下去\n下去,我现在下去\n下去,下去\n下去,下去\n哦。 ..\n下降,现在下降\n下降,现在下降\n下降,现在下降\n下降,下降,现在下降,下降,下降,现在\n哦,哦34嵌入“

一句警告:API 根本不适合您的问题。 我相信没有办法真正快速地做你想做的事,可能将自己限制在前几名(20 名?也许 50 名?)结果之一,并且,可选地,不使用额外的艺术家 ID 查询将是 go 的方式用例。 否则,您将不得不让用户等待,并可能通过让等待变得更有趣来应对。 我在测试时也遇到过几次请求超时,请记住这一点,这样您的机器人就不会彻底崩溃。

完整运行以上代码(我对其进行了限制,以免对 API 施加太多影响):

7.91 s ± 848 ms per loop (mean ± std. dev. of 3 runs, 1 loop each)

至少,这已经为搜索返回的顶级艺术家生成了一首真正随机歌曲的歌词(好吧,只要他们的歌曲少于max_page ...)。

如果前 50 首歌曲中的一首足够好,它就会变得容易得多,请求更少,速度大约是原来的两倍:

artist_id = genius.search_artists("Led")['sections'][0]['hits'][0]['result']['id']
songs = genius.artist_songs(artist_id, per_page=50, sort='popularity')['songs']
lyrics = genius.lyrics(random.choice(songs)['id'])

3.57 s ± 362 ms per loop (mean ± std. dev. of 3 runs, 1 loop each). 

Try-catch 块会进一步减慢你的速度(在我的测试中很容易超过半秒)。 请记住,一首随机歌曲可能根本没有歌词。

循环遍历所有歌曲并获取它们的原始实现仍然非常慢。 即使只有 10 首歌:

genius.search_artist("Led", max_songs=10)
[SNIP]
Done. Found 10 songs.
22.8 s ± 987 ms per loop (mean ± std. dev. of 3 runs, 1 loop each)

进一步的改进方法可能包括本地缓存,如 rabbibillclinton 建议的那样,或者通过查找每个艺术家的歌曲数量来变得聪明。 但这就是它的要点; 它不会跑得很快,但它可以跑得很好

这段代码会找到你想要的歌词,但它也会缓存艺术家的歌词,这样你就不必运行它两次,以防你之前已经检索到一位艺术家。

import json

genius = lyricsgenius.Genius(api_key) # Running outside the command so it only runs once (faster).
@client.command()
async def lyrics(ctx, *, artist_name): # Asterisk means it accepts more than one word as an argument.
    try:
        with open(artist_name + ' lyrics', 'r') as f:
            lyrics = json.load(f)
    except:
        try:
            artist = genius.search_songs(artist_name, sort="title")
            artist.save_lyrics(filename=(artist_name +  "lyrics"))
            with open(artist_name + ' lyrics', 'r') as f:
                lyrics = json.load(f)
        except Exception as e:
            await ctx.send("Failed to find artist.")
            print(e)
            return

这段代码没有等待包含艺术家的另一条消息,而是将其作为另一个参数接收,例如(假设您的前缀类似于“:”): !lyrics The Beatles

然后,该代码将在您目录中的现有文件中搜索名为“The Beatles lyrics.json”的 JSON 文件。 如果找到 JSON 文件,代码将只从那里获取歌词。 由于您没有从 Genius API 检索任何内容,因此代码运行速度会快得多。 如果用户输入的艺术家名称是新的,则不会存在 JSON 文件。 如果不存在文件,代码将提前 go 并使用您的 API 检索它,这将花费更长的时间,但它只需要执行一次。

上面的代码运行后,您可以自由地使用lyrics变量做任何您想做的事情。 希望它运行得更快。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM