def scan_file(self, item, file): """Try to open file with mutagen to extract information :param item: MPD uri descriptor (path without local music path) :param file: full file name :return: (uri, title, artist, album, genre, date, rating, original, time-dummy, size-dummy) """ write_gpio_pipe('2 flash 0.2') try: m = mutagen.File(file) except HeaderNotFoundError as e: # (re)try to open by extension ext = os.path.splitext(file)[-1].lower() try: if ext == '.mp3': m = mutagen.mp3.MP3(file) elif ext == '.ogg': m = mutagen.oggvorbis.OggVorbis(file) elif ext == '.mpc': m = mutagen.musepack.Musepack(file) elif ext == 'm4a': m = mutagen.mp4.MP4(file) elif ext == 'flac': m = mutagen.flac.FLAC(file) elif ext == 'wma': m = mutagen.asf.ASF(file) else: self.main.print_message(file) self.main.print_message('MUTAGEN ERROR {0}'.format(e)) m = None except HeaderNotFoundError as e: self.main.print_message(file) self.main.print_message('MUTAGEN ERROR 2 {0}'.format(e)) m = None except (FileNotFoundError, mutagen.MutagenError): self.main.print_message('File not found: ' + file) return None if m is None: self.main.print_message('NO TAGS FOR ' + file) return item, self.plain_filename( file), '', '', '', 0, 0, True, 0, 0, elif type(m) == mutagen.mp3.MP3: return self.parse_mp3(item, m, file) elif type(m) == mutagen.oggvorbis.OggVorbis: return self.parse_ogg(item, m, file) elif type(m) == mutagen.musepack.Musepack: return self.parse_mpc(item, m, file) elif type(m) == mutagen.flac.FLAC: return self.parse_ogg(item, m, file) elif type(m) == mutagen.mp4.MP4: return self.parse_m4a(item, m, file) elif type(m) == mutagen.asf.ASF: return self.parse_wma(item, m, file) self.main.print_message('UNKNOWN TYPE: ' + file) self.main.print_message(file) self.main.print_message(m) return item, self.plain_filename(file), '', '', '', 0, 0, True, 0, 0,
def mpc_idle(self): """Idle until event received from MPD. Note: this is blocking. To break this loop, toggle some switch on MPD. """ self.ensure_connected() res = [''] if self.connected: res = self.client.idle() self.check_party_mode(res) if 'update' in res and 'updating_db' not in self.client.status(): # let ratings scanner do rescan if database updated self.main.rescan_ratings = True # clear yellow LED write_gpio_pipe('1 0') elif 'update' in res: # raise yellow LED write_gpio_pipe('1 1') if 'playlist' in res: # Tell playlist view to update its status. redis_publisher = RedisPublisher(facility='piremote', broadcast=True) redis_publisher.publish_message(RedisMessage('playlist')) if 'player' not in res: return # Publish current player state via websocket via redis broadcast. state = self.client.status() cur = self.client.currentsong() state_data = dict(event=res) msg = json.dumps(state_data) redis_publisher = RedisPublisher(facility='piremote', broadcast=True) redis_publisher.publish_message(RedisMessage(msg)) # Check if playing and file changed --> insert into history if so. if 'state' in state and state['state'] == 'play': file = cur['file'] if file != self.last_file: self.last_file = file if 'artist' in cur and 'title' in cur: title = '%s - %s' % (cur['artist'], cur['title']) elif 'title' in cur: title = cur['title'] else: no_ext = os.path.splitext(file)[0] title = os.path.basename(no_ext).replace('_', ' ') self.insert_into_history(file, title)
def do_upload(self, filename): """Check if file can be uploaded and perform upload. Remove leading entry from PB_UPLOAD_SOURCES settings from filename. Add PB_UPLOAD_DIR to destination file name. :param filename: full path of file to be uploaded. :return: True if file uploaded """ self.main.print_message('Uploading %s' % filename) # Check if src_size = os.path.getsize(filename) s = os.statvfs(self.upload_path) free = s.f_bavail * s.f_frsize # keep at least 200MB (logs, cache, whatever) if src_size > free - 1024 * 1024 * 200: self.main.print_message('DRIVE FULL, NOT UPLOADING') return False name = filename # get pure filename (without prefix) for src in self.upload_sources: if filename.startswith(src): name = filename.replace(src, '') if name.startswith('/'): name = name[1:] dest = os.path.join(self.upload_path, name) if os.path.isfile(dest): if os.path.getsize(dest) == src_size: # File exists - no upload return False # check dir exists dirname = os.path.dirname(dest) if not os.path.isdir(dirname): try: os.makedirs(dirname) except OSError as e: self.main.print_message('MKDIR FAILED: ' + dirname) self.main.print_message("OSError: {0}".format(e)) return False write_gpio_pipe('2 1') # raise red led # perform copy try: shutil.copy(filename, dest) except IOError as e: self.main.print_message('COPY FAILED: ' + dest) self.main.print_message("IOError: {0}".format(e)) return False write_gpio_pipe('2 0') # clear red led return True
def rescan(self): """Check if new items in MPD database which are not in SQL database. Read tags and append to database if so. Remove items from SQL database which are no longer in MPD database. Called via main daemon loop, triggered via MPD_Idler. Does not block too much, will leave scanning loop if keep_run is False in main. """ self.main.print_message("RESCANNING RATINGS") rescan_broken = False # set to True if add chunk is too large if len(self.to_add) == 0 and len(self.to_remove) == 0: write_gpio_pipe("1 1\n3 1") # raise yellow and blue led mpd_files = self.get_mpd_files() if mpd_files is None or len(mpd_files) == 0: # There was a mpd connect error, retry next loop. # Or: the music db was completely wiped out. Do nothing until there are files again. return # Get mpd files which are not in database not_in_db = self.get_db_files(not_in_database=mpd_files) if not_in_db is None: # there was an db read error, retry next loop return music_path = self.get_music_path() if music_path is None: # there was an mpd error, retry next loop return self.to_add = [] for item in not_in_db: filename = os.path.join(music_path, item) mp3_info = self.scan_file(item, filename) if mp3_info is not None: self.to_add.append(mp3_info) if not self.main.keep_run: # worker shut down in the meantime return if len(self.to_add) > 499: # add in chunks of 500 rescan_broken = True break too_many = self.get_db_files(not_in_list=mpd_files) self.to_remove = [] for item in too_many: self.to_remove.append((item, )) if not self.main.keep_run: # worker shut down in the meantime return if DEBUG: self.main.print_message('FOUND %d new files in mpd db' % len(self.to_add)) self.main.print_message( 'FOUND %d files in db which are not in mpd db' % len(self.to_remove)) write_gpio_pipe("1 0\n3 0") # clear yellow and blue led # Files to_add scanned and to_remove found # if len(self.to_add) > 0 and False: # does not work for large DBs. # read times from MPD database for new items to_add_times = [] write_gpio_pipe("1 1") # raise yellow led client = MPDClient() client.timeout = 10 try: client.connect('localhost', 6600) except ConnectionError: self.main.print_message("ERROR: CONNECT") return # retry next loop for add_item in self.to_add: if not self.main.keep_run: # worker shut down in the meantime return add = list(add_item) try: mpd_item = client.find('file', add_item[0]) except (ConnectionError, CommandError): self.main.print_message("ERROR: CONNECT") return # retry next loop if len(mpd_item) == 1 and 'time' in mpd_item[0]: try: length = int(mpd_item[0]['time']) except ValueError: length = 0 add[8] = length to_add_times.append(add) self.to_add = to_add_times write_gpio_pipe("1 0") # clear yellow led # times scanned from MPD # if len(self.to_add) > 0 or len(self.to_remove) > 0: # apply to SQL write_gpio_pipe("3 1") # raise blue led self.alter_db('insert_many', self.to_add) self.alter_db('remove_many', self.to_remove) write_gpio_pipe("3 0") # clear blue led self.main.rescan_ratings = rescan_broken # do not rerun on successful run self.to_add = [] # required to not block rating scanner for next run self.to_remove = []
def check_party_mode(self, res, force=False): """Check if party mode is on, append items to playlist/shrink playlist if needed. Fetch settings directly from SQL database of piremote App. This is some of the dirtiest hacks possible for process communication, but works for this purpose. DB settings: party_mode: ['0', '1'] '1' if use party mode -> auto extend playlist party_remain: 'int' number of songs to keep in playlist above current song. party_low_water: 'int' if this number of songs left, append. party_high_water: 'int' when appending, append until this number of songs left. """ playlist_event = force for ev in res: if ev == 'player' or ev == 'playlist': playlist_event = True if not playlist_event: return # Fetch from database. # Note: this is how IPC is performed here -- not nice, but works. party_mode = False party_low_water = 10 party_high_water = 20 party_remain = 10 db = DATABASES['default'] conn = psycopg2.connect(dbname=db['NAME'], user=db['USER'], password=db['PASSWORD'], host=db['HOST']) cur = conn.cursor() cur.execute('''SELECT key, value FROM piremote_setting''') for row in cur.fetchall(): if row[0] == 'party_mode': party_mode = row[1] == '1' if row[0] == 'party_remain': party_remain = int(row[1]) if row[0] == 'party_low_water': party_low_water = int(row[1]) if row[0] == 'party_high_water': party_high_water = int(row[1]) cur.close() conn.close() if not party_mode: return status = self.client.status() pos = int(status['song']) if 'song' in status else 0 pl_len = int(status['playlistlength']) pl_remain = max(pl_len - pos - 1, 0) # Append randomly until high_water mark reached, if below low water mark. if pl_remain < party_low_water: write_gpio_pipe("4 1") # raise white led pl_add = max(party_high_water - pl_remain, 0) db_files = self.client.list('file') for i in range(pl_add): self.client.add(db_files[random.randrange(0, len(db_files))]) write_gpio_pipe("4 0") # clear white led # Shrink playlist until 'remain' songs left before current item. if pos > party_remain: for i in range(pos - party_remain): self.client.delete(0)