diff options
author | colin <colin@cicadas.surf> | 2023-04-08 12:13:32 -0700 |
---|---|---|
committer | colin <colin@cicadas.surf> | 2023-04-08 12:13:32 -0700 |
commit | 6183e4448eda7416e63436e0614c781260c68654 (patch) | |
tree | eb1aa321ef02ae55badd14240a1e196654e2422a /files/bandleader.py | |
parent | e217ceafff5766ba06dfa16aa80ae615c4537109 (diff) |
altered .gitignore; added preview files
Diffstat (limited to 'files/bandleader.py')
-rw-r--r-- | files/bandleader.py | 271 |
1 files changed, 271 insertions, 0 deletions
diff --git a/files/bandleader.py b/files/bandleader.py new file mode 100644 index 0000000..16e167d --- /dev/null +++ b/files/bandleader.py @@ -0,0 +1,271 @@ +from selenium.webdriver import Firefox +from selenium.webdriver.firefox.options import Options +from collections import namedtuple, Counter +from threading import Thread +from time import sleep, ctime +from os.path import isfile +import csv + + +BANDCAMP_FRONTPAGE='https://bandcamp.com/' + +TrackRec = namedtuple('TrackRec', + ['track_url', # if available + 'title', + 'artist', + 'artist_url', + 'album', + 'album_url', + 'timestamp' # when you played it + ]) + +class BandLeader(): + + def __init__(self,csvpath=None): + + self.database_path=csvpath + self.database = [] # using a list b/c its simple + + # load database from disk if possible + if isfile(self.database_path): + with open(self.database_path, newline='') as dbfile: + dbreader = csv.reader(dbfile) + next(dbreader) # to ignore the header line + self.database = [TrackRec._make(rec) for rec in dbreader] + + # create a headless browser + opts = Options() + opts.set_headless() + self.browser = Firefox(options=opts) + self.browser.get(BANDCAMP_FRONTPAGE) + self.paused = True + + self.autoplay_on = False + self._current_track_record = None + self._current_track_number = 1 + # create a thread that periodically maintains our database by + # consulting the browser's current state + self._has_quit = False + self.thread = Thread(target=self._maintain) + self.thread.daemon = True # kills the thread when the main process dies + self.thread.start() + + self.tracks() + + def _maintain(self): + while not self._has_quit: + self._update_db() + self._check_auto_advance() + sleep(1) + + + def log(self,where, err): + print(where, err) + + + def save_db(self): + with open(self.database_path,'w',newline='') as dbfile: + dbwriter = csv.writer(dbfile) + dbwriter.writerow(list(TrackRec._fields)) + for entry in self.database: + dbwriter.writerow(list(entry)) + + + def _update_db(self): + try: + check = (self._current_track_record is not None + and self._current_track_record is not None + and (len(self.database) == 0 + or self.database[-1] != self._current_track_record) + and self.is_playing()) + if check: + self.database.append(self._current_track_record) + self.save_db() + + except Exception as e: + self.log('_update_db',e) + + + def _check_auto_advance(self): + try: + if self.autoplay_on and not self.is_playing(): + self.play_next() + except Exception as e: + self.log('_check_auto_advance', e) + + + def toggle_autoplay(self): + self.autoplay_on = not self.autoplay_on + if self.autoplay_on: + print('autoplay is ON') + else: + print('autoplay is OFF') + + + def currently_playing(self): + ''' + returns the record for the currently playing track, + or None if nothing is playing + ''' + try: + if self.is_playing(): + track_title = self.browser.find_element_by_class_name('title').text + album_detail = self.browser.find_element_by_css_selector('.detail-album > a') + album_title = album_detail.text + album_url = album_detail.get_attribute('href').split('?')[0] + artist_detail = self.browser.find_element_by_css_selector('.detail-artist > a') + artist = artist_detail.text + artist_url = artist_detail.get_attribute('href').split('?')[0] + return TrackRec('',track_title,artist,artist_url,album_title,album_url,ctime()) + except Exception as e: + print('there was an error: {}'.format(e)) + + return None + + + def tracks(self): + ''' + lists the tracks that are presently available for play and + associates a track number with each one. You may use these + track numbers to as arguments to the `play` method. + ''' + sleep(1) + discover_section = self.browser.find_element_by_class_name('discover-results') + left_x = discover_section.location['x'] + right_x = left_x + discover_section.size['width'] + + discover_items = self.browser.find_elements_by_class_name('discover-item') + self.track_list = [t for t in discover_items + if t.location['x'] >= left_x and t.location['x'] < right_x] + + for (i,track) in enumerate(self.track_list): + print('[{}]'.format(i+1)) + lines = track.text.split('\n') + print('Album : {}'.format(lines[0])) + print('Artist : {}'.format(lines[1])) + if len(lines) > 2: + print('Genre : {}'.format(lines[2])) + + + def play(self,track=None): + ''' + plays a track. If `track` is not supplied, this method + simulates pressing the play button somewhere on bandcamp's site. + + If `track` is an `int`, this method plays the track with that + track number. See the `tracks` method. + ''' + + if track is None: + self.browser.find_element_by_class_name('playbutton').click() + elif type(track) is int and track <= len(self.track_list) and track >= 1: + self._current_track_number = track + self.track_list[self._current_track_number - 1].click() + + sleep(0.5) + if self.is_playing(): + self._current_track_record = self.currently_playing() + print("CURRENTLY PLAYING") + self.print_track() + + def pause(self): + self.play() + self.paused = True + + + def resume(self): + if self.paused: + self.play() + self.paused = False + + + def is_playing(self): + ''' + returns `True` if a track is presently playing + ''' + playbtn = self.browser.find_element_by_class_name('playbutton') + return playbtn.get_attribute('class').find('playing') > -1 + + + def play_next(self): + ''' + plays the next available track + ''' + if self._current_track_number < len(self.track_list): + self.play(self._current_track_number+1) + else: + self.more_tracks() + self.play(1) + + + def play_prev(self): + ''' + plays the previous available track + ''' + if (self._current_track_number - 1) >= 0: + self.play(self._current_track_number -1) + + + def top_tracks(self,num=10): + ''' + lists the top `num` tracks in order of frequency of listening + ''' + c = Counter(t.title for t in self.database) + return c.most_common(num) + + def top_albums(self,num=10): + c = Counter(t.album for t in self.database) + return c.most_common(num) + + def top_artists(self,num=10): + c = Counter(t.artist for t in self.database) + return c.most_common(num) + + def catalogue_pages(self): + ''' + print the available pages in the catalogue that are presently + accessible + ''' + print('PAGES') + for e in self.browser.find_elements_by_class_name('item-page'): + print(e.text) + print('') + + + def more_tracks(self,page='next'): + ''' + finds more tracks in a contextual way. + + If on the main page, advances the listing in the 'Discover' + section. + + If on an album page, looks to the next album in the + 'Discography' section for the present artist. + ''' + next_btn = [e for e in self.browser.find_elements_by_class_name('item-page') + if e.text.lower().strip() == str(page)] + + if next_btn: + next_btn[0].click() + self.tracks() + + def explore(self): + ''' + visits the start page and lists tracks + ''' + self.browser.get(BANDCAMP_FRONTPAGE) + self.tracks() + + + def quit(self): + self._has_quit = True + self.browser.close() + # flush db to disk + + def print_track(self,tr=None): + if tr is None: + tr = self._current_track_record + + if tr is not None: + print('{}\n by {}\n on the album {}'.format(tr.title,tr.artist,tr.album)) + |