135 lines
4.1 KiB
Python
135 lines
4.1 KiB
Python
""" 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()
|
|
topic = None
|
|
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://musician.social')
|
|
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
|