You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

120 lines
3.5 KiB

  1. """ Runner for the Lo-Fi Beats bot at https://botsin.space/@lofibeats """
  2. import logging
  3. import logging.handlers
  4. import os
  5. import pickle
  6. import random
  7. import isodate
  8. import mastodon
  9. import youtube
  10. logging.basicConfig(
  11. handlers=[logging.handlers.RotatingFileHandler('beatbot.log', maxBytes=100000, backupCount=10),
  12. logging.StreamHandler()],
  13. level=logging.INFO,
  14. format="[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s",
  15. datefmt='%Y-%m-%dT%H:%M:%S')
  16. LOGGER = logging.getLogger(__name__)
  17. def randline(fname):
  18. """ Get a random line from a file """
  19. with open(fname) as afile:
  20. line = next(afile)
  21. for num, aline in enumerate(afile, 2):
  22. if random.randrange(num) == 0:
  23. line = aline
  24. return line.strip()
  25. def format_details(yt_id, snippet):
  26. ''' format the details of a video '''
  27. text = f'https://youtube.com/watch?v={yt_id}'
  28. try:
  29. if snippet['liveBroadcastContent'] == 'live':
  30. duration = ' (LIVE)'
  31. else:
  32. duration = ''
  33. details = youtube.get_details(yt_id)
  34. if details:
  35. delta = isodate.parse_duration(details['duration'])
  36. hours, rem = divmod(delta.seconds, 3600)
  37. minutes, seconds = divmod(rem, 60)
  38. duration = ' ('
  39. if hours:
  40. duration += f'{hours:d}:'
  41. duration += f'{minutes:02d}:{seconds:02d})'
  42. text += f' — {snippet["title"]}{duration}'
  43. except RuntimeError:
  44. LOGGER.exception("Unable to get video details")
  45. return text
  46. def bot():
  47. """ Run the bot """
  48. genre = randline('genres.txt')
  49. verb1 = randline('1syllableverbs.txt')
  50. verb2 = randline('2syllableverbs.txt')
  51. search_term = f'+lofi {genre} +beats {verb1} {verb2}'
  52. text = f"lo-fi {genre} beats to {verb1} and {verb2} to".replace(' ', ' ')
  53. seen_vids = set()
  54. try:
  55. with open('seen-vids.dat', 'rb') as seen:
  56. seen_vids = pickle.load(seen)
  57. except FileNotFoundError:
  58. LOGGER.info("Seen database not found")
  59. except pickle.UnpicklingError:
  60. LOGGER.exception("Seen database corrupted")
  61. videos = youtube.get_videos(search_term)
  62. LOGGER.info("%s: Found %d videos (%d new)", search_term,
  63. len(videos), len({id for id,_ in videos} - seen_vids))
  64. yt_id = None
  65. random.shuffle(videos)
  66. # try to find a random video we haven't used before
  67. for (vid, snippet) in videos:
  68. if vid not in seen_vids:
  69. yt_id = vid
  70. LOGGER.info("Using [https://youtube.com/watch?v=%s] %s",
  71. yt_id, snippet['title'])
  72. break
  73. if not yt_id and videos:
  74. # nothing new, so just select a random one (list is already shuffled)
  75. (yt_id, snippet) = videos[0]
  76. LOGGER.info("Reusing [https://youtube.com/watch?v=%s] %s",
  77. yt_id, snippet['title'])
  78. if yt_id:
  79. text += '\n\n' + format_details(yt_id, snippet)
  80. # remember that we used it already
  81. seen_vids.add(yt_id)
  82. LOGGER.info(text)
  83. if not os.environ.get('BEATBOT_TESTING'):
  84. mdon = mastodon.Mastodon(
  85. access_token='token.secret',
  86. api_base_url='https://botsin.space')
  87. mdon.status_post(text)
  88. with open('seen-vids.dat', 'wb') as seen:
  89. pickle.dump(seen_vids, seen)
  90. if __name__ == '__main__':
  91. try:
  92. bot()
  93. except Exception:
  94. LOGGER.exception("Uncaught exception")
  95. raise