""" Runner for the Lo-Fi Beats bot at https://botsin.space/@lofibeats """ import html import logging import logging.handlers import os import pickle import random import isodate import mastodon import youtube logging.basicConfig( handlers=[logging.handlers.RotatingFileHandler('beatbot.log', maxBytes=100000, backupCount=10), logging.StreamHandler()], level=logging.INFO, format="[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s", datefmt='%Y-%m-%dT%H:%M:%S') LOGGER = logging.getLogger(__name__) def randline(fname): """ Get a random line from a file """ with open(fname, encoding='utf-8') as afile: line = next(afile) for num, aline in enumerate(afile, 2): if random.randrange(num) == 0: line = aline return line.strip() def format_details(yt_id, snippet): ''' format the details of a video ''' text = f'https://youtube.com/watch?v={yt_id}' try: LOGGER.info("snippet: %s", snippet) if 'liveBroadcastContent' in snippet and snippet['liveBroadcastContent'] == 'live': duration = ' (LIVE)' else: duration = '' details = youtube.get_details(yt_id) LOGGER.info("details: %s", details) if details and 'duration' in details: delta = isodate.parse_duration(details['duration']) hours, rem = divmod(delta.seconds, 3600) minutes, seconds = divmod(rem, 60) if hours: duration = f' ({hours:d}:{minutes:02d}:{seconds:02d})' elif minutes or seconds: duration = f' ({minutes:d}:{seconds:02d})' text += f' — {html.unescape(snippet["title"])}{duration}' except RuntimeError: LOGGER.exception("Unable to get video details") return text def bot(): """ Run the bot """ seen_vids = set() try: with open('seen-vids.dat', 'rb') as seen: seen_vids = pickle.load(seen) except FileNotFoundError: LOGGER.info("Seen database not found") except pickle.UnpicklingError: LOGGER.exception("Seen database corrupted") for _ in range(5): genre = randline('genres.txt') noun = randline('nouns.txt') verb1 = randline('1syllableverbs.txt') verb2 = randline('2syllableverbs.txt') search_term = f'+lofi {genre} +{noun} {verb1} {verb2}' text = f"lo-fi {genre} {noun} to {verb1} and {verb2} to".replace(' ', ' ') region, topic = randline('yt_topics.txt').split() LOGGER.info("Searching in region %s (topicId=%s)", region, topic) videos = youtube.get_videos(search_term, regionCode=region, topicId=topic, safeSearch="strict") LOGGER.info("%s: Found %d videos (%d new)", search_term, len(videos), len({id for id, _ in videos} - seen_vids)) if videos: break yt_id = None random.shuffle(videos) # try to find a random video we haven't used before for (vid, snippet) in videos: if vid not in seen_vids: yt_id = vid LOGGER.info("Using [https://youtube.com/watch?v=%s] %s", yt_id, snippet['title']) break if not yt_id and videos: # nothing new, so just select a random one (list is already shuffled) (yt_id, snippet) = videos[0] LOGGER.info("Reusing [https://youtube.com/watch?v=%s] %s", yt_id, snippet['title']) if yt_id: text += '\n\n' + format_details(yt_id, snippet) # remember that we used it already seen_vids.add(yt_id) LOGGER.info(text) if not os.environ.get('BEATBOT_TESTING'): mdon = mastodon.Mastodon( access_token='token.secret', api_base_url='https://botsin.space') mdon.status_post(text) with open('seen-vids.dat', 'wb') as seen: pickle.dump(seen_vids, seen) if __name__ == '__main__': try: bot() except Exception: LOGGER.exception("Uncaught exception") raise