def postprocess_albums(self): log.debug(u"Postprocessing albums ...") s = session_get() found = 0 for album in s.query(Album).filter_by(deleted=False): # Stop if quite is requested if not self._run: s.rollback() return # Just jump over PK 1, this is the unknown album if album.id == 1: continue # skip albums that already have an artist if album.artist != 1: continue # See if all tracks in album have matching artist test = None found_artist = True for track in s.query(Track).filter_by(album=album.id): if test is None: test = track.artist else: if test != track.artist: found_artist = False if found_artist: # If we got here, all tracks in the album had the same artist. # Therefore, set album artist as this. found += 1 album.artist = test s.commit() s.close() log.debug(u"Found artist for %d new albums", found)
def on_auth_msg(self, packet_msg): sid = packet_msg.get('sid', '') s = session_get() user = None session = None try: session = s.query(Session).filter_by(key=sid).one() user = s.query(User).filter_by(id=session.user).one() except NoResultFound: pass s.close() # Session found with token. if session and user: self.sid = sid self.authenticated = True log.info(u"Authenticated with '%s'.", self.sid) # Send login success message self.send_message('auth', { 'uid': user.id, 'sid': sid, 'level': user.level }) return self.send_error('auth', "Invalid session", 403) log.warning(u"Authentication failed.")
def handle_delete(self, path): track = None cover = None # If it does NOT, make sure it is removed from index # First, see if track exists with this path. If not, see if it is a cover. # If neither, stop here. s = session_get() try: track = s.query(Track).filter_by(file=path, deleted=False).one() except NoResultFound: try: cover = s.query(Cover).filter_by(file=path, deleted=False).one() except NoResultFound: return s.close() # If we found a cover, remove it and clear references to it from albums if cover: self.handle_cover_delete(cover) return # If we found a track, remove it from any albums. If the albums are now empty, remove them. # If artist does not belong to any track, remove it also if track: self.handle_track_delete(track) return
def handle_cover_delete(self, cover): s = session_get() for album in s.query(Album).filter_by(cover=cover.id, deleted=False): album.cover = 1 s.query(Cover).filter_by(id=cover.id, deleted=False).update({'deleted': True, 'updated': utc_now()}) s.commit() s.close()
def get(self, session_id, size_flag, cover_id): s = session_get() # Make sure session is valid try: s.query(Session).filter_by(key=session_id).one() except NoResultFound: s.close() self.set_status(401) self.finish("401") log.warning(u"Cover ID %d requested without a valid session.", cover_id) return # Find the cover we want try: cover = s.query(Cover).filter_by(id=cover_id).one() except NoResultFound: s.close() self.set_status(404) self.finish("404") log.warning(u"Cover ID %s does not exist.", cover_id) return s.close() if size_flag == "0": cover_file = os.path.join(settings.COVER_CACHE_DIRECTORY, "{}_small.jpg".format(cover.id)) elif size_flag == "1": cover_file = os.path.join(settings.COVER_CACHE_DIRECTORY, "{}_medium.jpg".format(cover.id)) else: # Make sure we have a filename if not cover.file: self.set_status(404) self.finish("404") log.warning(u"Cover file for ID %d is not set.", cover_id) return cover_file = cover.file # Just pick content type and dump out the file. self.set_header("Content-Type", mimetypes.guess_type("file://" + cover_file)[0]) try: with file(cover_file, 'rb') as f: ret = yield gen.Task(self.get_data, f) self.write(ret) except IOError: self.set_status(404) self.finish("404") log.warning(u"Matching file for cover ID %d does not exist.", cover_id) return self.finish()
def postprocess_covers(self): log.debug(u"Postprocessing covers ...") found = 0 s = session_get() for album in s.query(Album).filter_by(deleted=False): # Stop if quit is requested if not self._run: s.rollback() return # If album already has a cover, keep going if album.cover != 1: continue # Jump over PK 1, this is the unknown album if album.id == 1: continue # Try to find cover art for this album for track in s.query(Track).filter_by(album=album.id): mdir = s.query(Directory).get(track.dir) cover_art = self._cover_art.get(mdir.directory, None) if cover_art: cover = get_or_create(s, Cover, file=cover_art[0], deleted=False) found += 1 # Make thumbnails try: img = Image.open(cover_art[0]) size = 200, 200 out_file = os.path.join(settings.COVER_CACHE_DIRECTORY, '{}_small.jpg'.format(cover.id)) img.thumbnail(size, Image.ANTIALIAS) img.save(out_file, "JPEG") except IOError: log.error(u"Unable to create a small thumbnail for cover ID %d", cover.id) try: img = Image.open(cover_art[0]) size = 800, 800 out_file = os.path.join(settings.COVER_CACHE_DIRECTORY, '{}_medium.jpg'.format(cover.id)) img.thumbnail(size, Image.ANTIALIAS) img.save(out_file, "JPEG") except IOError: log.error(u"Unable to create a medium thumbnail for cover ID %d", cover.id) # Set new cover id for album, and update tracks and the album timestamp for sync s.query(Track).filter_by(album=album.id).update({'updated': utc_now()}) s.query(Album).filter_by(id=album.id).update({'updated': utc_now(), 'cover': cover.id}) # Cover lookup done for this album, continue with next break # That's that, commit changes for this album s.commit() s.close() self._cover_art = {} # Clear cover art cache log.debug(u"Found and attached %d new covers.", found)
def sync_table(self, name, table, remote_ts, push=False): # Send message containing all new data in the table s = session_get() self.send_message('sync', { 'query': 'request', 'table': name, 'ts': to_isodate(utc_now()), 'push': push, 'data': [t.serialize() for t in s.query(table).filter(table.updated > remote_ts)] }) s.close()
def get(self, session_id, size_flag, cover_id): s = session_get() # Make sure session is valid try: s.query(Session).filter_by(key=session_id).one() except NoResultFound: s.close() self.set_status(401) self.finish("401") log.warning(u"Cover ID %d requested without a valid session.", cover_id) return # Find the cover we want try: cover = s.query(Cover).filter_by(id=cover_id).one() except NoResultFound: s.close() self.set_status(404) self.finish("404") log.warning(u"Cover ID %s does not exist.", cover_id) return s.close() if size_flag == "0": cover_file = os.path.join(settings.COVER_CACHE_DIRECTORY, "{}_small.jpg".format(cover.id)) elif size_flag == "1": cover_file = os.path.join(settings.COVER_CACHE_DIRECTORY, "{}_medium.jpg".format(cover.id)) else: # Make sure we have a filename if not cover.file: self.set_status(404) self.finish("404") log.warning(u"Cover file for ID %d is not set.", cover_id) return cover_file = cover.file # Just pick content type and dump out the file. self.set_header("Content-Type", mimetypes.guess_type("file://"+cover_file)[0]) try: with file(cover_file, 'rb') as f: ret = yield gen.Task(self.get_data, f) self.write(ret) except IOError: self.set_status(404) self.finish("404") log.warning(u"Matching file for cover ID %d does not exist.", cover_id) return self.finish()
def on_logout_msg(self, packet_msg): # Remove session s = session_get() s.query(Session).filter_by(key=self.sid).delete() s.commit() s.close() # Dump out log log.info(u"Logged out '%s'.", self.sid) # Deauthenticate & clear session ID self.authenticated = False self.sid = None
def preprocess_deleted(self): s = session_get() log.debug(u"Removing deleted tracks from database ...") for track in s.query(Track).filter_by(deleted=False): if not os.path.isfile(track.file): self.handle_track_delete(track) log.debug(u"Removing deleted covers from database ...") for cover in s.query(Cover).filter_by(deleted=False): if cover.id == 1: continue if not os.path.isfile(cover.file): self.handle_cover_delete(cover) s.close()
def sync_table(self, name, table, remote_ts, push=False): # Send message containing all new data in the table s = session_get() self.send_message( 'sync', { 'query': 'request', 'table': name, 'ts': to_isodate(utc_now()), 'push': push, 'data': [ t.serialize() for t in s.query(table).filter(table.updated > remote_ts) ] }) s.close()
def handle_track_delete(self, track): s = session_get() if track.album != 1: # If album only has a single (this) track, remove album if s.query(Track).filter_by(album=track.album, deleted=False).count() == 0: s.query(Album).filter_by(id=track.album, deleted=False).update({'deleted': True, 'updated': utc_now()}) if track.artist != 1: # If artist only has a single (this) track, remove artist if s.query(Track).filter_by(artist=track.artist, deleted=False).count() == 0: s.query(Artist).filter_by(id=track.artist, deleted=False).update({'deleted': True, 'updated': utc_now()}) # That's that, delete the track. s.query(Track).filter_by(id=track.id, deleted=False).update({'deleted': True, 'updated': utc_now()}) # Save changes s.commit() s.close()
def on_login_msg(self, packet_msg): username = packet_msg.get('username', '') password = packet_msg.get('password', '') s = session_get() try: user = s.query(User).filter_by(username=username).one() except NoResultFound: self.send_error('login', 'Incorrect username or password', 401) log.warning(u"Invalid username or password in login request.") s.close() return # If user exists and password matches, pass onwards! if user and pbkdf2_sha256.verify(password, user.password): session_id = generate_session() # Add new session ses = Session(key=session_id, user=user.id) s.add(ses) s.commit() # Mark connection as authenticated, and save session id self.sid = session_id self.authenticated = True # Dump out log log.info(u"Logged in '%s'.", self.sid) # TODO: Cleanup old sessions # Send login success message self.send_message('login', { 'uid': user.id, 'sid': session_id, 'level': user.level }) else: self.send_error('login', 'Incorrect username or password', 401) log.warning(u"Invalid username or password in login request.") s.close()
def get(self, session_id, song_id): s = session_get() # Make sure session is valid try: s.query(Session).filter_by(key=session_id).one() except NoResultFound: s.close() self.set_status(401) self.finish("401") log.warning(u"Track ID %d requested without a valid session.", song_id) return # Find the song we want try: song = s.query(Track).filter_by(id=song_id).one() except NoResultFound: s.close() self.set_status(404) self.finish("404") log.warning(u"Nonexistent track ID %d requested.", song_id) return s.close() # See if we got range range_bytes = self.request.headers.get('Range') range_start = 0 range_end = None if range_bytes: range_start, range_end = range_bytes[6:].split("-") range_end = None if range_end is "" else int(range_end) range_start = int(range_start) # Set streaming headers self.set_status(206) self.set_header("Accept-Ranges", "bytes") # Find content length and type if song.type in settings.NO_TRANSCODE_FORMATS: size = song.bytes_len song_file = song.file self.set_header("Content-Type", mimetypes.guess_type("file://"+song.file)[0]) else: song_file = os.path.join( settings.MUSIC_CACHE_DIRECTORY, "{}.{}".format(song.id, settings.TRANSCODE_FORMAT)) size = song.bytes_tc_len self.set_header("Content-Type", "audio/mpeg") # Set end range if not range_end or range_end >= size: range_end = size-1 # Make sure range_start and range_end are withing size limits if range_start >= size: self.set_status(416) self.finish() return # Stream out try: with open(song_file, 'rb') as f: # Set range headers left = (range_end+1) - range_start self.set_header("Content-Length", left) self.set_header("Content-Range", "bytes {}-{}/{}".format(range_start, range_end, size)) self.flush() # Forward to starting position and start reading data f.seek(range_start) while left: r = 16384 if 16384 < left else left data = yield gen.Task(self.get_data, (f, r)) self.write(data) left -= r except IOError: self.set_status(404) self.finish("404") log.error(u"Requested track ID %d doesn't exist.", song.id) return # Flush the last bytes before finishing up. self.flush() try: self.finish() except HTTPOutputError, o: log.error(u"Error while serving track ID %d: %s.", song_id, str(o))
def get(self, session_id, song_id): s = session_get() # Make sure session is valid try: s.query(Session).filter_by(key=session_id).one() except NoResultFound: s.close() self.set_status(401) self.finish("401") log.warning(u"Track ID %d requested without a valid session.", song_id) return # Find the song we want try: song = s.query(Track).filter_by(id=song_id).one() except NoResultFound: s.close() self.set_status(404) self.finish("404") log.warning(u"Nonexistent track ID %d requested.", song_id) return s.close() # See if we got range range_bytes = self.request.headers.get('Range') range_start = 0 range_end = None if range_bytes: range_start, range_end = range_bytes[6:].split("-") range_end = None if range_end is "" else int(range_end) range_start = int(range_start) # Set streaming headers self.set_status(206) self.set_header("Accept-Ranges", "bytes") # Find content length and type if song.type in settings.NO_TRANSCODE_FORMATS: size = song.bytes_len song_file = song.file self.set_header("Content-Type", mimetypes.guess_type("file://" + song.file)[0]) else: song_file = os.path.join( settings.MUSIC_CACHE_DIRECTORY, "{}.{}".format(song.id, settings.TRANSCODE_FORMAT)) size = song.bytes_tc_len self.set_header("Content-Type", "audio/mpeg") # Set end range if not range_end or range_end >= size: range_end = size - 1 # Make sure range_start and range_end are withing size limits if range_start >= size: self.set_status(416) self.finish() return # Stream out try: with open(song_file, 'rb') as f: # Set range headers left = (range_end + 1) - range_start self.set_header("Content-Length", left) self.set_header( "Content-Range", "bytes {}-{}/{}".format(range_start, range_end, size)) self.flush() # Forward to starting position and start reading data f.seek(range_start) while left: r = 16384 if 16384 < left else left data = yield gen.Task(self.get_data, (f, r)) self.write(data) left -= r except IOError: self.set_status(404) self.finish("404") log.error(u"Requested track ID %d doesn't exist.", song.id) return # Flush the last bytes before finishing up. self.flush() try: self.finish() except HTTPOutputError, o: log.error(u"Error while serving track ID %d: %s.", song_id, str(o))
def on_playlist_msg(self, packet_msg): if not self.authenticated: return query = packet_msg.get('query', '') # Creates a new playlist with a given name. Errors out if the name already exists. if query == 'add_playlist': name = packet_msg.get('name') s = session_get() if s.query(Playlist).filter_by(name=name, deleted=False).count() > 0: self.send_error('playlist', "Playlist with given name already exists", 500) log.warning(u"Playlist with given name already exists.") else: playlist = Playlist(name=name, updated=utc_now()) s.add(playlist) s.commit() self.sync_table('playlist', Playlist, utc_minus_delta(5), push=True) log.debug(u"A new playlist created!") s.close() return # Delete playlist and all related items if query == 'del_playlist': playlist_id = packet_msg.get('id') if id > 1: s = session_get() s.query(PlaylistItem).filter_by(playlist=playlist_id, deleted=False).update({ 'deleted': True, 'updated': utc_now() }) s.query(Playlist).filter_by(id=playlist_id).update({ 'deleted': True, 'updated': utc_now() }) s.commit() s.close() self.sync_table('playlist', Playlist, utc_minus_delta(5), push=True) self.sync_table('playlistitem', PlaylistItem, utc_minus_delta(5), push=True) self.notify_playlist_changes(playlist_id) log.debug(u"Playlist and items deleted!") return # Copy scratchpad playlist (id 1) to a new playlist if query == 'copy_scratchpad': to_id = packet_msg.get('id') s = session_get() s.query(PlaylistItem).filter_by(playlist=to_id, deleted=False).update({ 'deleted': True, 'updated': utc_now() }) s.commit() for item in s.query(PlaylistItem).filter_by(playlist=1, deleted=False): plitem = PlaylistItem(track=item.track, playlist=to_id, number=item.number, updated=utc_now()) s.add(plitem) s.commit() s.close() self.sync_table('playlistitem', PlaylistItem, utc_minus_delta(5), push=True) self.notify_playlist_changes(to_id) log.debug(u"Playlist copied!") return # Saves tracks to the given playlist. Clears existing tracks. if query == 'save_playlist': playlist_id = packet_msg.get('id') items = packet_msg.get('tracks') s = session_get() s.query(PlaylistItem).filter_by(playlist=playlist_id, deleted=False).update({ 'deleted': True, 'updated': utc_now() }) k = 0 for item in items: plitem = PlaylistItem(track=item['id'], playlist=playlist_id, number=k, updated=utc_now()) s.add(plitem) k += 1 s.commit() s.close() self.sync_table('playlistitem', PlaylistItem, utc_minus_delta(5), push=True) self.notify_playlist_changes(playlist_id) log.debug(u"Playlist updated!") return
def handle_audio(self, path, ext, is_audiobook): s = session_get() # If track already exists and has not changed, stop here # Otherwise either edit or create new track entry fsize = os.path.getsize(path) try: track = s.query(Track).filter_by(file=path).one() if fsize == track.bytes_len: s.close() return except NoResultFound: track = Track(file=path, album=1, artist=1, type=ext[1:]) # Attempt to open up the file in Mutagen for tag information m = None try: m = mutagen.File(path) except: log.warning(u"Could not read header for %s", path) # Set correct sizes track.bytes_len = fsize track.bytes_tc_len = 0 if m: # Find artist track_artist = self._get_tag(m, ('TPE1', u'©ART', 'Author', 'Artist', 'ARTIST', 'TXXX:ARTIST' 'TRACK ARTIST', 'TRACKARTIST', 'TrackArtist', 'Track Artist', 'artist')) # Find album artist album_artist = self._get_tag(m, ('TPE2', u'aART', 'TXXX:ALBUM ARTIST', 'TXXX:ALBUMARTIST', 'ALBUM ARTIST', 'ALBUMARTIST', 'AlbumArtist', 'Album Artist')) # Find album title album_title = self._get_tag(m, (u'©alb', 'TALB', 'ALBUM', 'album', 'TXXX:ALBUM')) # Find title track_title = self._get_tag(m, (u'©nam', 'TXXX:TITLE', 'TIT2', 'Title', 'TITLE', 'TRACK TITLE', 'TRACKTITLE', 'TrackTitle', 'Track Title')) # Find track number track_tags = ('TRCK', 'TXXX:TRACK', 'Track', 'trkn', 'TRACK', 'tracknumber', 'TRACKNUMBER') track_number = self._get_tag_tuple(m, track_tags) if type(track_number) == tuple: track.track = int(track_number[0]) else: track_number = self._get_tag(m, track_tags) if '/' in track_number: track.track = int(track_number.split('/')[0]) elif track_number: track.track = int(track_number) # Find disc number disc_tags = ('TXXX:DISCNUMBER', 'discnumber', 'DISCNUMBER', 'TPOS') track_disc = self._get_tag_tuple(m, disc_tags) if type(track_disc) == tuple: track.disc = int(track_disc[0]) else: track_disc = self._get_tag(m, disc_tags) if '/' in track_disc: track.disc = int(track_disc.split('/')[0]) elif track_disc: track.disc = int(track_disc) # Find Genre/Content type track.genre = self._get_tag(m, ('TCON', u'@gen', 'gnre', 'Genre', 'genre', 'GENRE', 'TXXX:GENRE')) # Find date track.date = self._get_tag(m, ('TYER', 'TDAT', 'TDRC', 'TDRL', u'@day', 'date', "DATE", "YEAR", "Date", "Year", 'TXXX:YEAR')) if track.date is None: track.date = u"" # Set track title, if found if track_title: track.title = track_title elif track.track and album_title: track.title = album_title + u" " + str(track.track) elif track.track: track.title = "Track " + str(track.track) else: track.title = os.path.splitext(os.path.basename(path))[0] # If there is a track artist, add it to its own model if track_artist: artist = get_or_create(s, Artist, name=track_artist, deleted=False) track.artist = artist.id elif album_artist: artist = get_or_create(s, Artist, name=album_artist, deleted=False) track.artist = artist.id # If there is no track title or artist, try to parse the filename if not track.title or not track.artist: filename = os.path.splitext(os.path.basename(path))[0] m_artist, m_title = match_track_filename(filename) if not track.title and m_title: track.title = m_title if not track.artist and m_artist: track.artist = m_artist # Looks for album with given information if album_title: if album_artist: a_artist = get_or_create(s, Artist, name=album_artist, deleted=False) else: a_artist = s.query(Artist).get(1) # Set album try: album = s.query(Album).filter_by(title=album_title, artist=a_artist.id, deleted=False).one() track.album = album.id except NoResultFound: album = Album(title=album_title, artist=a_artist.id, cover=1, is_audiobook=is_audiobook) s.add(album) s.commit() track.album = album.id # Set dir bpath = os.path.dirname(path) mdir = get_or_create(s, Directory, directory=bpath, deleted=False) track.dir = mdir.id # Save everything s.add(track) s.commit() # Check if we need to transcode if track.type not in settings.NO_TRANSCODE_FORMATS: self.transcode(s, track) s.close()