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))