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.

125 lines
3.8 KiB

""" 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 """
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(' ', ' ')
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")
videos = youtube.get_videos(search_term)
LOGGER.info("%s: Found %d videos (%d new)", search_term,
len(videos), len({id for id, _ in videos} - seen_vids))
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