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.
 
 
 

124 lines
3.7 KiB

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