def remove_song_from_playlist(playlistid, songid): """Removes a given song to a specified playlist. Arguments: playlistid (str): Integer string identifying a unique playlist. songid (str): Integer string identifying a unique song. """ with access_db() as db_conn: try: song = db_conn.query(Song).get(songid) playlist = db_conn.query(Playlist).get(playlistid) except: logger.warn( f"Exception encountered while trying to access db to remove song {songid} from playlist {playlistid}" ) return else: if not song: logger.warn(f"Song {songid} does not exist.") return if not playlist: logger.warn(f"Playlist {playlistid} does not exist.") return try: playlist.songs.remove(song) except ValueError: pass else: db_conn.commit()
def add_song_to_playlist(playlistid, songid): """Adds a given song to a specified playlist. Arguments: playlistid (str): Integer string identifying a unique playlist. songid (str): Integer string identifying a unique song. """ with access_db() as db_conn: try: song = db_conn.query(Song).get(songid) playlist = db_conn.query(Playlist).get(playlistid) except: logger.warn( f"Exception encountered while trying to access db to add song {songid} to playlst {playlistid}" ) return else: if not song: logger.warn(f"Song {songid} does not exist.") return if not playlist: logger.warn(f"Playlist {playlistid} does not exist.") return playlist.songs.append(song) db_conn.commit()
def update_username(input_uuid, username): """Update the username of a given user. Because we will support username changes, this is how that will be accomplished. Arguments: input_uuid (uuid): UUID associated with the account that should have its username changed. username (str): Value to change the account's username to. Returns: success (bool): True if the username was changed successfully, False otherwise. """ success = False if username_taken(username): logger.critical( f'Attempted to update username of user with UUID {input_uuid} to {username}, even though it was taken already.' ) return success with access_db() as db_conn: result = db_conn.query(User).\ get(input_uuid) if not user: logger.critical( f'Attempted to update username for user that doesn\'t exist with UUID {input_uuid}.' ) return success result.username = username db_conn.commit() success = True return success return success
def get_playlists_for_user(user_guid): """Fetch public playlists, and playlists owned by a specific user. Arguments: user_guid (uuid): UUID identifying the specific user to fetch playlists for. """ result = {'playlists': []} with access_db() as db_conn: accessible = db_conn.query(Playlist)\ .filter( or_(Playlist.owner_guid==user_guid, Playlist.public==True) ) for playlist in accessible: owner_name = db_conn.query(User.username)\ .filter(User.guid==user_guid)\ .scalar() data = { 'id': playlist.id, 'name': playlist.name, 'owner_name': owner_name, 'public': playlist.public, } result['playlists'].append(data) return result
def owns_playlist(playlistid, owner_guid): """Check if a given user owns a specific playlist. Arguments: playlistid (str): Integer string identifying the playlist. owner_guid (uuid): UUID identifying the user to check against. """ # Ensure both playlist and owner_guid are not None if (not playlistid) or (not owner_guid): logger.warn( f"Trying to check ownership with invalid playlistid of owner_guid." ) return False with access_db() as db_conn: try: playlist = db_conn.query(Playlist).get(playlistid) except: logger.warn( f"Exception encountered while trying to access playlist id: {playlistid}." ) return False else: if not playlist: logger.warn(f"No playlist found with id {playlistid}.") return False if playlist.owner_guid == owner_guid: return True logger.warn( f"User with guid {owner_guid} attempted to modify playlist they do not own with id {playlistid}." ) return False
def fetch_track_info(songid): """Fetch detailed information about a given track. Arguments: songid (str): Integer string identifying the track to fetch information on. """ with access_db() as db_conn: try: track = db_conn.query(Song).get(songid) except: logger.warn( f"Exception encountered while trying to access song id: {songid}" ) return None else: if not track: logger.warn(f"No song found with id {songid}.") return None return { 'title': track.track_name, 'artist': track.artist_name, 'album': track.album_name, 'track_length': track.track_length, 'id': track.id, }
def refresh_database_thread(): """Walk through all files in the music folder, adding them to the database as necessary.""" logger.info('Started refreshing.') try: # This handles directory walking, it's kind of nasty to use this iterator for dirpath, dirname, filename in os.walk(MUSIC_FOLDER): for f in filename: # Do nothing with non-mp3 tracks if not f.endswith('.mp3'): continue full_file_path = os.path.join(dirpath, f) track_info = load_track_data(full_file_path) if track_info: add_track_to_database(track_info) except Exception as e: logger.warn('Exception encountered while refreshing database.') logger.warn(e) raise e finally: # Return to a non-refreshing state once finished. with access_db() as db_conn: try: state = db_conn.query(RefreshState).one() state.is_refreshing = False db_conn.commit() except MultipleResultsFound as e: state = db_conn.query(RefreshState).first().delete() db_conn.commit() logger.info('Refreshing finished!') return logger.info('Refreshing finished!')
def clean_database(): """Remove all missing tracks from the database.""" with access_db() as db_conn: missing_tracks = db_conn.query(Song)\ .filter(Song.file_missing == True) for track in missing_tracks: db_conn.delete(track) db_conn.commit()
def get_all_admins(): """Fetch all admin emails from database.""" result = [] with access_db() as db_conn: all_admins = db_conn.query(User).filter(User.admin == True).all() for admin in all_admins: result.append((admin.email, admin.username)) return result
def remove_track_from_database(track_id): """Remove a song from the database.""" with access_db() as db_conn: track = db_conn.query(Song).get(track_id) if track: db_conn.delete(track) db_conn.commit() return True return False
def create_new_playlist(playlist_name, owner_guid, owner_name): """Create a new playlist for a user. Arguments: playlist_name (str): Name for the new playlist. owner_guid (uuid): UUID of the user that is creating the playlist. """ with access_db() as db_conn: new_playlist = Playlist(name=playlist_name, owner_guid=owner_guid) db_conn.add(new_playlist) db_conn.commit()
def label_track_missing(track_path, missing): """Update the track_missing flag in the database. Arguments: track_path (str): The path to the track that is missing. """ with access_db() as db_conn: track = db_conn.query(Song)\ .filter(Song.track_path==track_path)\ .first() if track: track.file_missing = missing db_conn.commit()
def get_playlist_data_from_id(playlistid): """Fetch information about a given playlist, as well as its tracks, from a unique id. Arguments: playlistid (str): Integer string identifying a unique playlist. Returns: playlist_data (dict): Dictionary containing information about the playlist, as well as its contents. """ if not playlistid: logger.warn(f"Trying to access playlist without id.") return None with access_db() as db_conn: try: playlist = db_conn.query(Playlist)\ .get(playlistid) except: logger.warn( f"Exception encountered while trying to access playlist with id {playlistid}" ) return None else: if not playlist: return None owner_name = db_conn.query(User.username)\ .filter(User.guid==playlist.owner_guid)\ .scalar() playlist_data = { 'tracks': [], 'owner_name': owner_name, 'name': playlist.name, 'public': playlist.public, } if playlist.songs: # Sorts by artist name, then album name, then track name. sorted_songs = sorted( playlist.songs, key=lambda x: ((x.artist_name and x.artist_name.lower() or ''), (x.album_name and x.album_name.lower() or ''), (x.track_name and x.track_name.lower() or ''))) for track in sorted_songs: track_info = { 'title': track.track_name, 'artist': track.artist_name, 'album': track.album_name, 'id': track.id, 'length': track.track_length } playlist_data['tracks'].append(track_info) return playlist_data
def fetch_user_by_uuid(input_uuid): """Fetch the user entry in the database associated with the provided UUID. Arguments: input_uuid (UUID): The UUID to look up. Returns: result (User or None): The User object associated with the provided UUID if it exists, else None. """ with access_db() as db_conn: result = db_conn.query(User).\ filter(User.guid==input_uuid).\ first() return result
def fetch_user_by_username(username): """Fetch the user entry in the database associated with the provided username. Arguments: username (str): The username to look up. Returns: result (User or None): The User object associated with the provided username if it exists, else None. """ with access_db() as db_conn: result = db_conn.query(User).\ filter(User.username==username).\ first() return result
def fetch_user_by_email(email): """Fetch the user entry in the database associated with the provided email. Arguments: email (str): The email account to lookup. Returns: result (User or None): The User object associated with the provided email if it exists, else None. """ with access_db() as db_conn: result = db_conn.query(User).\ filter(User.email==email).\ first() return result
def log_login_attempt(email, success, ip): """Create a database entry when a user attempts to login. Arguments: email (str): The email the login is attempted for. success (str): Whether the login was successful or not. ip (ipaddress): The ip address that the login attempt occurred from. """ with access_db() as db_conn: curr_time = datetime.datetime.now() login_attempt = LoginAttempt(email=email, success=success, source_ip=ip, attempt_time=curr_time) db_conn.add(login_attempt) db_conn.commit()
def username_taken(username): """Check if a given username already exists in the database. Arguments: username (str): The username to look up. Returns: result (bool): Whether or not the user exists in the database. """ with access_db() as db_conn: result = db_conn.query(User).\ filter(User.username==username).\ first() if result: return True return False
def create_full_user(email, username, password): """Creates a user in the database given an email, username, and password. Arguments: email (str): The email to register the account with. username (str): The username to register the account with. password (str): The password to register the account with. """ with access_db() as db_conn: generated_uuid = uuid.uuid1() storable_password_hash = create_storable_password(password) user = User(email=email, guid=generated_uuid, username=username, password_hash=storable_password_hash) db_conn.add(user) db_conn.commit() return generated_uuid
def fetch_track_hash(songid): """Fetch the file hash for a given track id. Arguments: songid (str): Integer string identifying the song to fetch the file hash for. """ with access_db() as db_conn: try: track = db_conn.query(Song).get(songid) except: logger.warn( f"Exception encountered while trying to access song id: {songid}" ) return None else: if not track: logger.warn(f"No song found with id {songid}.") return None return track.track_hash
def get_playlists_owned_by_user(user_guid): """Fetch playlists owned by a specific user. Arguments: user_guid (uuid): UUID identifying the specific user to fetch playlists for. """ result = {'playlists': []} with access_db() as db_conn: accessible = db_conn.query(Playlist)\ .filter(Playlist.owner_guid==user_guid) for playlist in accessible: data = { 'id': playlist.id, 'name': playlist.name, 'owner_name': playlist.owner_name, 'public': playlist.public, } result['playlists'].append(data) return result
def check_file_missing(songid): """Check whether a specific track's file is missing in the database. Arguments: songid (int): ID for the track to be checked. """ with access_db() as db_conn: try: track = db_conn.query(Song).get(songid) except: logger.warn( f"Exception encountered while trying to access song id: {songid}" ) return None else: if not track: logger.warn(f"No song found with id {songid}.") return None return track.file_missing
def set_playlist_publicity(playlistid, publicity): """Set a playlist to public or private.""" with access_db() as db_conn: try: playlist = db_conn.query(Playlist).get(playlistid) except: logger.warn( f'Exception encountered while trying to fetch playlist {playlistid}' ) return False else: if not playlist: logger.warn(f'Playlist {playlistid} does not exist.') return False playlist.public = publicity db_conn.commit() return True
def get_playlist_from_id(playlistid): """Fetch the ORM object for a given playlist id. Arguments: playlistid (str): Integer string identifying a unique playlist. """ if not playlistid: logger.warn(f"Trying to access playlist without id.") return None with access_db() as db_conn: try: playlist = db_conn.query(Playlist).get(playlistid) except: logger.warn( f"Exception encountered while trying to access playlist with id {playlistid}" ) return None else: return playlist
def get_public_playlists(): """Fetch publicly available playlists.""" result = {'playlists': []} with access_db() as db_conn: # Filter all playlists by publicity. public = db_conn.query(Playlist)\ .filter(Playlist.public==True) for playlist in public: # Fetch the playlist owner in order to get their username owner_name = db_conn.query(User.username)\ .filter(User.guid==playlist.owner_guid)\ .scalar() data = { 'id': playlist.id, 'name': playlist.name, 'owner_name': owner_name, 'public': playlist.public, } result['playlists'].append(data) return result
def make_user_admin(email): """Make a user with a given email address an admin. Arguments: email (string): The email address of the user to enable admin privileges on. Returns: success (bool): True if the user exists, and was made an admin; false otherwise. """ success = False with access_db() as db_conn: result = db_conn.query(User).\ filter(User.email==email).\ first() try: result.admin = True db_conn.commit() success = True except AttributeError: logger.warning('Attempted to make a non-existant user an admin.') return success
def update_password(input_uuid, old_password, new_password): """Update the password of a given user. Arguments: input_uuid (uuid): UUID associated with the account that should have its password changed. old_password (str): Old password of the user, used for verification of request. new_password (str): New password of the user. Returns: success (bool): True if password change was successful, False otherwise. """ # Fetch the user, fail if it doesn't exist. success = False user = fetch_user_by_uuid(input_uuid) if not user: return success # Make sure that the old password matches the stored password, fail if not. passwords_match = check_password(old_password, user.password_hash) if not passwords_match: return success # Generate a new hash, and store it for the user. storable_password_hash = create_storable_password(password) with access_db() as db_conn: user = db_conn.query(User).\ filter(User.guid==input_uuid).\ first() if not user: # This should never happen, unless there is a database race condition for some reason. # For safety sake, we make the check anyway. return success user.password_hash = storable_password_hash db_conn.commit() success = True return success return success
def get_all_tracks(): """Fetch track info for all songs in the database.""" with access_db() as db_conn: result = [] all_tracks = db_conn.query(Song).filter( Song.file_missing == False).all() # Sort by artist name, then album name, then track name. all_tracks.sort(key=lambda x: (x.artist_name and x.artist_name.lower( ) or '', x.album_name and x.album_name.lower() or '', x.track_name and x.track_name.lower() or '')) for track in all_tracks: track_info = { 'title': track.track_name, 'artist': track.artist_name, 'album': track.album_name, 'id': track.id, 'track_length': track.track_length } result.append(track_info) return result return None
def add_track_to_database(track_info): """Add a track to the database. Arguments: track_info (dict): Specific track information to be entered into the database. """ track_path = track_info['track_path'] track_hash = track_info['track_hash'] with access_db() as db_conn: # Make sure the track doesn't already exist # First by checking the track path exists = db_conn.query(Song)\ .filter(Song.track_path==track_path)\ .first() if exists: return False else: # Then by checking the track hash exists = db_conn.query(Song)\ .filter(Song.track_hash==track_hash)\ .first() if exists and exists.file_missing: # If the track hash exists, and the file is missing, update its path exists.track_path = track_path exists.file_missing = False db_conn.commit() return False # Create and add ORM object to the database song = Song(track_name=track_info['title'], artist_name=track_info['artist'], album_name=track_info['album'], track_path=track_info['track_path'], track_length=track_info['track_length'], track_hash=track_info['track_hash']) db_conn.add(song) db_conn.commit() return True return False
def refresh_database(): """Update the song database. Uses a single database entry to decide whether or not it's allowed to update the database. This is only allowed once every 5 minutes, at max. """ with access_db() as db_conn: try: entry = db_conn.query(RefreshState).one() except MultipleResultsFound as e: logger.info('Multiple rows found. Deleting and refreshing.') for db_entry in db_conn.query(RefreshState).all(): db_conn.delete(db_entry) db_conn.commit() except NoResultFound as e: logger.info('No rows found. Creating.') state = RefreshState(last_refresh=datetime.datetime.now()) db_conn.add(state) db_conn.commit() async_refresh() else: if entry.last_refresh: # If the database has been refreshed at least once, check that it's been 5 minutes. delta = datetime.datetime.now() - entry.last_refresh if delta > datetime.timedelta(minutes=5): # If 5 minutes have passed, allow an update. async_refresh() entry.last_refresh = datetime.datetime.now() db_conn.commit() else: logger.info( f'Not enough time has passed since last refresh. Time delta: {delta}' ) logger.info(f'Last refresh: {entry.last_refresh}') else: # If the database has never been refreshed, then go for it async_refresh() entry.last_refresh = datetime.datetime.now() db_conn.commit()