def fromString(self, value): """Read a value from the string *value* and store it in this option. The format of *value* depends on the type of the option, see ''export''.""" if self.type == 'string': return value elif self.type == 'int': try: return int(value) except ValueError: logging.warning( __name__, "Invalid int in delegate configuration in storage file.") return None elif self.type == 'bool': return value == 'True' elif self.type == 'tag': if tags.isInDb(value): return tags.get(value) elif self.type == 'datapiece': if value == 'none': return None else: try: return DataPiece.fromString(value) except ValueError: logging.warning( __name__, "Invalid datapiece in delegate configuration in storage file." ) return None
def fromString(self,value): """Read a value from the string *value* and store it in this option. The format of *value* depends on the type of the option, see ''export''.""" if self.type == 'string': return value elif self.type == 'int': try: return int(value) except ValueError: logging.warning(__name__, "Invalid int in delegate configuration in storage file.") return None elif self.type == 'bool': return value == 'True' elif self.type == 'tag': if tags.isInDb(value): return tags.get(value) elif self.type == 'datapiece': if value == 'none': return None else: try: return DataPiece.fromString(value) except ValueError: logging.warning(__name__, "Invalid datapiece in delegate configuration in storage file.") return None
def checkMPDChanges(self, changed): """Check for changes in the MPD subsystems listed in "changed" (as returned from idle). All changes will be handled accordingly. If """ self.mpdStatus = self.client.status() if 'error' in self.mpdStatus: from ...gui.dialogs import warning warning('MPD error', self.tr('MPD reported an error:\n{}').format(self.mpdStatus['error'])) self.client.clearerror() if 'mixer' in changed: self.updateMixer() changed.remove('mixer') if 'playlist' in changed: self.updatePlaylist() changed.remove('playlist') if 'player' in changed: self.updatePlayer() changed.remove('player') if 'update' in changed: changed.remove('update') if 'output' in changed: self.updateOutputs() changed.remove('output') if len(changed) > 0: logging.warning(__name__, 'unhandled MPD changes: {}'.format(changed))
def setIndex(self, index): """Undo/redo commands until there are *index* commands left that can be undone.""" if self._inUndoRedo or self.isComposing(): raise UndoStackError( "Cannot change index during undo/redo or while a macro is built." "") if index != self._index: if not 0 <= index <= len(self._commands): raise ValueError( "Invalid index {} (there are {} commands on the stack).". format(index, len(self._commands))) self._inUndoRedo = True try: if index < self._index: for command in reversed(self._commands[index:self._index]): command.undo() else: for command in self._commands[self._index:index]: command.redo() except Exception as e: logging.exception(__name__, "Exception during undo/redo.") self._clear() logging.warning(__name__, "Undostack cleared") return self._index = index self._emitQueuedEvents() self._inUndoRedo = False self._emitSignals()
def setIndex(self, index): """Undo/redo commands until there are *index* commands left that can be undone.""" if self._inUndoRedo or self.isComposing(): raise UndoStackError("Cannot change index during undo/redo or while a macro is built.""") if index != self._index: if not 0 <= index <= len(self._commands): raise ValueError("Invalid index {} (there are {} commands on the stack)." .format(index, len(self._commands))) self._inUndoRedo = True try: if index < self._index: for command in reversed(self._commands[index:self._index]): command.undo() else: for command in self._commands[self._index:index]: command.redo() except Exception as e: logging.exception(__name__, "Exception during undo/redo.") self._clear() logging.warning(__name__, "Undostack cleared") return self._index = index self._emitQueuedEvents() self._inUndoRedo = False self._emitSignals()
def __init__(self, state=None, **args): super().__init__(**args) layout = QtWidgets.QHBoxLayout(self) scene = DesktopScene(self) if state is not None: if 'items' in state: try: levels.real.collect([ t[0] for t in state['items'] if isinstance(t[0], int) ]) items = [] for t in state['items']: if t[0] == 'stack': item = StackItem.fromState(scene, t) else: item = CoverItem.fromState(scene, t) scene.addItem(item) except Exception as e: logging.warning(__name__, "Could not restore cover desk: " + str(e)) if 'domain' in state: scene.domain = domains.domainById(state['domain']) self.view = QtWidgets.QGraphicsView(scene) self.view.setAcceptDrops(True) self.view.setAlignment(Qt.AlignLeft | Qt.AlignTop) self.view.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag) #self.view.scene().selectionChanged.connect(self.selectionChanged) layout.addWidget(self.view) self.view.ensureVisible(0, 0, 1, 1, 0, 0)
def __init__(self, state=None, **args): super().__init__(**args) layout = QtWidgets.QHBoxLayout(self) scene = DesktopScene(self) if state is not None: if 'items' in state: try: levels.real.collect([t[0] for t in state['items'] if isinstance(t[0], int)]) items = [] for t in state['items']: if t[0] == 'stack': item = StackItem.fromState(scene, t) else: item = CoverItem.fromState(scene, t) scene.addItem(item) except Exception as e: logging.warning(__name__, "Could not restore cover desk: "+str(e)) if 'domain' in state: scene.domain = domains.domainById(state['domain']) self.view = QtWidgets.QGraphicsView(scene) self.view.setAcceptDrops(True) self.view.setAlignment(Qt.AlignLeft | Qt.AlignTop) self.view.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag) #self.view.scene().selectionChanged.connect(self.selectionChanged) layout.addWidget(self.view) self.view.ensureVisible(0, 0, 1, 1, 0, 0)
def __init__(self, tagList=None, state=None): if tagList is None: assert state is not None tagList = [tags.get(name) for name in state] if any(tag.type != tags.TYPE_VARCHAR for tag in tagList): logging.warning(__name__, "Only tags of type varchar are permitted in the browser's layers.") tagList = {tag for tag in tagList if tag.type == tags.TYPE_VARCHAR} self.tagList = tagList
def removeWidgetClass(widgetClassId): widgetClass = WidgetClass.getWidgetClass(widgetClassId) if widgetClass is None: logging.warning(__name__, "Attempt to remove nonexistent widget class: {}".format(widgetClassId)) else: WidgetClass.__registeredClasses.remove(widgetClass) if application.mainWindow is not None: application.mainWindow._widgetClassRemoved(widgetClass)
def addWidgetClass(widgetClass=None, **kwargs): if widgetClass is None: widgetClass = WidgetClass(**kwargs) if widgetClass in WidgetClass.widgetClasses(): logging.warning(__name__, "Attempt to add widget class twice: {}".format(widgetClass)) else: WidgetClass.__registeredClasses.append(widgetClass) if application.mainWindow is not None: application.mainWindow._widgetClassAdded(widgetClass) return widgetClass
def __init__(self, tagList=None, state=None): if tagList is None: assert state is not None tagList = [tags.get(name) for name in state] if any(tag.type != tags.TYPE_VARCHAR for tag in tagList): logging.warning( __name__, "Only tags of type varchar are permitted in the browser's layers." ) tagList = {tag for tag in tagList if tag.type == tags.TYPE_VARCHAR} self.tagList = tagList
def removeWidgetClass(widgetClassId): widgetClass = WidgetClass.getWidgetClass(widgetClassId) if widgetClass is None: logging.warning( __name__, "Attempt to remove nonexistent widget class: {}".format( widgetClassId)) else: WidgetClass.__registeredClasses.remove(widgetClass) if application.mainWindow is not None: application.mainWindow._widgetClassRemoved(widgetClass)
def addWidgetClass(widgetClass=None, **kwargs): if widgetClass is None: widgetClass = WidgetClass(**kwargs) if widgetClass in WidgetClass.widgetClasses(): logging.warning( __name__, "Attempt to add widget class twice: {}".format(widgetClass)) else: WidgetClass.__registeredClasses.append(widgetClass) if application.mainWindow is not None: application.mainWindow._widgetClassAdded(widgetClass) return widgetClass
def fallbackHash(self, path): """Compute the audio hash of a single file using ffmpeg to dump the audio. This method uses the "ffmpeg" binary ot extract the first 15 seconds in raw PCM format and then creates the MD5 hash of that data. """ try: ans = subprocess.check_output(['ffmpeg', '-i', path, '-v', 'quiet', '-f', 's16le', '-t', '15', '-']) return 'hash:{}'.format(hashlib.md5(ans).hexdigest()) except OSError: logging.warning(__name__, 'ffmpeg not installed - could not compute fallback audio hash') except subprocess.CalledProcessError: logging.warning(__name__, 'ffmpeg run failed')
def dropEvent(self, event): mimeData = event.mimeData() if not Qt.CopyAction & event.possibleActions(): return event.setDropAction(Qt.CopyAction) if mimeData.hasFormat(config.options.gui.mime): elements = [w.element for w in mimeData.toplevelWrappers()] level = mimeData.level else: logging.warning(__name__, "Invalid drop event (supports only {})" .format(", ".join(mimeData.formats()))) return self.addElements(elements, event.scenePos()) event.acceptProposedAction()
def dropEvent(self, event): mimeData = event.mimeData() if not Qt.CopyAction & event.possibleActions(): return event.setDropAction(Qt.CopyAction) if mimeData.hasFormat(config.options.gui.mime): elements = [w.element for w in mimeData.toplevelWrappers()] level = mimeData.level else: logging.warning( __name__, "Invalid drop event (supports only {})".format( ", ".join(mimeData.formats()))) return self.addElements(elements, event.scenePos()) event.acceptProposedAction()
def addPath(self, element): if element.isFile(): if not element.inParentLevel(): logging.warning(__name__, '{} not in parent level'.format(element)) return oldPath = element.inParentLevel().url.path newPath = element.url.path if oldPath != newPath: self.addCenter(delegates.TextItem(self.tr("From: {}").format(oldPath), self.newPathStyleOld)) self.newRow() self.addCenter(delegates.TextItem(self.tr("To: {}").format(newPath), self.newPathStyleNew)) else: self.addCenter(delegates.TextItem(self.tr("Unchanged: {}").format(oldPath), self.unchangedStyle))
def readTags(self): """Load the tags from disk using pytaglib. Special tags (tracknumber, compilation, discnumber) are stored in the "specialTags" attribute. """ self.tags = tags.Storage() self.specialTags = collections.OrderedDict() try: self._taglibFile = taglib.File(self.url.path) except OSError: if self.url.extension in config.options.main.audio_extensions: logging.warning( __name__, 'TagLib failed to open "{}". Tags will be stored in database only' .format(self.url.path)) return self.length = self._taglibFile.length autoProcessingDone = False for key, values in self._taglibFile.tags.items(): key = key.lower() if key in self.specialTagNames: self.specialTags[key] = values elif key in config.options.tags.auto_delete: autoProcessingDone = True continue elif key in autoReplaceTags: autoProcessingDone = True key = autoReplaceTags[key] elif tags.isValidTagName(key): tag = tags.get(key) validValues = [] for string in values: try: validValues.append(tag.convertValue(string, crop=True)) except tags.TagValueError: logging.error( __name__, "Invalid value for tag '{}' found: {}".format( tag.name, string)) if len(validValues) > 0: self.tags.add(tag, *validValues) else: logging.error( __name__, "Invalid tag name '{}' found : {}".format(key, self.url)) if autoProcessingDone: self.saveTags()
def redo(self): """Redo the next command/macro.""" if self._inUndoRedo or self.isComposing(): raise UndoStackError("Cannot redo a command during undo/redo or while a macro is built.""") if self._index == len(self._commands): raise UndoStackError("There is no command to redo.") self._inUndoRedo = True commandOrMacro = self._commands[self._index] try: commandOrMacro.redo() except Exception: logging.exception(__name__, "Exception during redo.") self._clear() logging.warning(__name__, "Undostack cleared") self._index += 1 self._emitQueuedEvents() self._inUndoRedo = False self._emitSignals()
def fallbackHash(self, path): """Compute the audio hash of a single file using ffmpeg to dump the audio. This method uses the "ffmpeg" binary ot extract the first 15 seconds in raw PCM format and then creates the MD5 hash of that data. """ try: ans = subprocess.check_output([ 'ffmpeg', '-i', path, '-v', 'quiet', '-f', 's16le', '-t', '15', '-' ]) return 'hash:{}'.format(hashlib.md5(ans).hexdigest()) except OSError: logging.warning( __name__, 'ffmpeg not installed - could not compute fallback audio hash') except subprocess.CalledProcessError: logging.warning(__name__, 'ffmpeg run failed')
def redo(self): """Redo the next command/macro.""" if self._inUndoRedo or self.isComposing(): raise UndoStackError( "Cannot redo a command during undo/redo or while a macro is built." "") if self._index == len(self._commands): raise UndoStackError("There is no command to redo.") self._inUndoRedo = True commandOrMacro = self._commands[self._index] try: commandOrMacro.redo() except Exception: logging.exception(__name__, "Exception during redo.") self._clear() logging.warning(__name__, "Undostack cleared") self._index += 1 self._emitQueuedEvents() self._inUndoRedo = False self._emitSignals()
def readTags(self): """Load the tags from disk using pytaglib. Special tags (tracknumber, compilation, discnumber) are stored in the "specialTags" attribute. """ self.tags = tags.Storage() self.specialTags = collections.OrderedDict() try: self._taglibFile = taglib.File(self.url.path) except OSError: if self.url.extension in config.options.main.audio_extensions: logging.warning(__name__, 'TagLib failed to open "{}". Tags will be stored in database only' .format(self.url.path)) return self.length = self._taglibFile.length autoProcessingDone = False for key, values in self._taglibFile.tags.items(): key = key.lower() if key in self.specialTagNames: self.specialTags[key] = values elif key in config.options.tags.auto_delete: autoProcessingDone = True continue elif key in autoReplaceTags: autoProcessingDone = True key = autoReplaceTags[key] elif tags.isValidTagName(key): tag = tags.get(key) validValues = [] for string in values: try: validValues.append(tag.convertValue(string, crop=True)) except tags.TagValueError: logging.error(__name__, "Invalid value for tag '{}' found: {}".format(tag.name, string)) if len(validValues) > 0: self.tags.add(tag, *validValues) else: logging.error(__name__, "Invalid tag name '{}' found : {}".format(key, self.url)) if autoProcessingDone: self.saveTags()
def handleInitialScan(self): """Called when the initial filesystem walk is finished. Removes newfiles that have not been found anymore, adds newly found files, stores a list of missing committed files, and updates folder states if necessary. """ # add newly found files to newfiles table (also creating folders entries) newfiles = [] hashRequests = [] for path, stamp in self.fsFiles.items(): if path in self.files: file = self.files[path] else: url = urls.URL.fileURL(path) file = self.addFile(urls.URL.fileURL(path), store=False) newfiles.append(file) if file.hash is None or (file.id is None and file.verified < stamp): # for files with outdated verified that are in DB, we need to check if tags have changed # which is done later in the scan process hashRequests.append(HashRequest(priority=int(file.id is None), path=path)) self.storeNewFiles(newfiles) # remove entries in newfiles that don't exist anymore self.removeFiles([file for path, file in self.files.items() if file.id is None and path not in self.fsFiles]) # store missing DB files for later usage self.missingDB = [file for path, file in self.files.items() if file.id and path not in self.fsFiles] if len(self.missingDB): logging.warning(__name__, '{} files in DB missing on filesystem'.format(len(self.missingDB))) # compute missing hashes, if necessary if len(hashRequests): logging.info(__name__, 'Hash value of {} files missing'.format(len(hashRequests))) self.scanState = ScanState.computingHashes self.hashThread.lastJobDone.clear() for elem in hashRequests: self.hashThread.jobQueue.put(elem) self.scanTimer.start(5000) # check hash results every 5 seconds else: self.scanCheckModified()
def addPath(self, element): if element.isFile(): if not element.inParentLevel(): logging.warning(__name__, '{} not in parent level'.format(element)) return oldPath = element.inParentLevel().url.path newPath = element.url.path if oldPath != newPath: self.addCenter( delegates.TextItem( self.tr("From: {}").format(oldPath), self.newPathStyleOld)) self.newRow() self.addCenter( delegates.TextItem( self.tr("To: {}").format(newPath), self.newPathStyleNew)) else: self.addCenter( delegates.TextItem( self.tr("Unchanged: {}").format(oldPath), self.unchangedStyle))
def dropEvent(self, event): mimeData = event.mimeData() if mimeData.hasFormat(config.options.gui.mime): allElements = (w.element for w in mimeData.wrappers()) level = mimeData.level elif mimeData.hasUrls(): allElements = levels.real.collect(url for url in event.mimeData().urls() if url.isValid() and url.scheme() == 'file' and os.path.exists(url.toLocalFile())) level = levels.real else: logging.warning(__name__, "Invalid drop event (supports only {})" .format(", ".join(mimeData.formats()))) return elements = [] ids = set() for element in allElements: if element.id not in ids: ids.add(element.id) elements.append(element) self.setElements(level, elements) event.acceptProposedAction()
def updatePlaylist(self): """Update the playlist if it has changed on the server. Currently, two special cases are detected: Insertion of consecutive songs, and removal of consecutive songs. In any other case, a complete playlist change is issued. """ newVersion = int(self.mpdStatus["playlist"]) if newVersion == self.playlistVersion: return logging.debug(__name__, "detected new plVersion: {}-->{}".format(self.playlistVersion, newVersion)) if self.playlistVersion is None: # this happens only on initialization. self.mpdPlaylist = [x["file"] for x in self.client.playlistinfo()] self.playlistVersion = newVersion self.playlist.initFromUrls(self.makeUrls(self.mpdPlaylist)) return plChanges = self.client.plchanges(self.playlistVersion) changedFiles = [ a["file"] for a in plChanges ] self.playlistVersion = newVersion newLength = int(self.mpdStatus["playlistlength"]) # first special case: find out if only consecutive songs were removed if newLength < len(self.mpdPlaylist): numRemoved = len(self.mpdPlaylist) - newLength oldSongsThere = self.mpdPlaylist[-len(plChanges):] if len(plChanges) > 0 else [] if changedFiles == oldSongsThere: firstRemoved = newLength - len(plChanges) del self.mpdPlaylist[firstRemoved:firstRemoved+numRemoved] self.playlist.removeByOffset(firstRemoved, numRemoved, updateBackend='onundoredo') return # second special case: find out if a number of consecutive songs were inserted elif newLength > len(self.mpdPlaylist): numInserted = newLength - len(self.mpdPlaylist) numShifted = len(plChanges) - numInserted if numShifted == 0: newSongsThere = [] oldSongsThere = [] else: newSongsThere = plChanges oldSongsThere = self.mpdPlaylist[-numShifted:] if newSongsThere == oldSongsThere: firstInserted = len(self.mpdPlaylist) - numShifted paths = changedFiles[:numInserted] self.mpdPlaylist[firstInserted:firstInserted] = paths urls = self.makeUrls(paths) pos = int(plChanges[0]["pos"]) self.playlist.insertUrlsAtOffset(pos, urls, updateBackend='onundoredo') return if len(plChanges) == 0: logging.warning(__name__, 'no changes???') return # other cases: update self.mpdPlaylist and perform a general playlist change reallyChange = False for pos, file in sorted((int(a["pos"]),a["file"]) for a in plChanges): if pos < len(self.mpdPlaylist): if self.mpdPlaylist[pos] != file: reallyChange = True self.mpdPlaylist[pos] = file else: reallyChange = True self.mpdPlaylist.append(file) if reallyChange: # this might not happen e.g. when a stream is updated self.playlist.resetFromUrls(self.makeUrls(self.mpdPlaylist), updateBackend='onundoredo')
def __call__(self, path): if not maestro.utils.files.isMusicFile(path): return 'nomusic' try: data = subprocess.check_output(['fpcalc', path], stderr=subprocess.DEVNULL) except OSError: # fpcalc not found, not executable etc. global _logOSError if _logOSError: _logOSError = False # This error will always occur - don't print it again. logging.warning(__name__, 'Error computing AcoustID fingerprint: fpcalc unavailable?') return self.fallbackHash(path) except subprocess.CalledProcessError: # fpcalc returned non-zero exit status logging.warning(__name__, f'Error computing AcoustID fingerprint of {path}: fpcalc returned non-zero exit status') return self.fallbackHash(path) data = data.decode(sys.getfilesystemencoding()) try: duration, fingerprint = (line.split("=", 1)[1] for line in data.splitlines() ) except Exception as e: logging.warning(__name__, f'Error computing AcoustID fingerprint of {path}: {e}') return self.fallbackHash(path) import urllib.request, urllib.error, json try: req = urllib.request.urlopen(self.requestURL.format(self.apikey, duration, fingerprint)) except (urllib.error.HTTPError, urllib.error.URLError) as e: logging.warning(__name__, 'Error opening {}'.format(self.requestURL.format(self.apikey, duration, fingerprint))) return self.fallbackHash(path) ans = req.read().decode('utf-8') req.close() ans = json.loads(ans) if ans['status'] != 'ok': logging.warning(__name__, 'Error retrieving AcoustID fingerprint for "{}"'.format(path)) return self.fallbackHash(path) results = ans['results'] if len(results) == 0: logging.warning(__name__, 'No AcoustID fingerprint found for "{}"'.format(path)) return self.fallbackHash(path) bestResult = max(results, key=lambda x: x['score']) if "recordings" in bestResult and len(bestResult["recordings"]) > 0: ans = "mbid:{}".format(bestResult["recordings"][0]["id"]) else: ans = "acoustid:{}".format(bestResult["id"]) return ans
def __call__(self, path): if not maestro.utils.files.isMusicFile(path): return 'nomusic' try: data = subprocess.check_output(['fpcalc', path], stderr=subprocess.DEVNULL) except OSError: # fpcalc not found, not executable etc. global _logOSError if _logOSError: _logOSError = False # This error will always occur - don't print it again. logging.warning( __name__, 'Error computing AcoustID fingerprint: fpcalc unavailable?') return self.fallbackHash(path) except subprocess.CalledProcessError: # fpcalc returned non-zero exit status logging.warning( __name__, f'Error computing AcoustID fingerprint of {path}: fpcalc returned non-zero exit status' ) return self.fallbackHash(path) data = data.decode(sys.getfilesystemencoding()) try: duration, fingerprint = (line.split("=", 1)[1] for line in data.splitlines()) except Exception as e: logging.warning( __name__, f'Error computing AcoustID fingerprint of {path}: {e}') return self.fallbackHash(path) import urllib.request, urllib.error, json try: req = urllib.request.urlopen( self.requestURL.format(self.apikey, duration, fingerprint)) except (urllib.error.HTTPError, urllib.error.URLError) as e: logging.warning( __name__, 'Error opening {}'.format( self.requestURL.format(self.apikey, duration, fingerprint))) return self.fallbackHash(path) ans = req.read().decode('utf-8') req.close() ans = json.loads(ans) if ans['status'] != 'ok': logging.warning( __name__, 'Error retrieving AcoustID fingerprint for "{}"'.format(path)) return self.fallbackHash(path) results = ans['results'] if len(results) == 0: logging.warning( __name__, 'No AcoustID fingerprint found for "{}"'.format(path)) return self.fallbackHash(path) bestResult = max(results, key=lambda x: x['score']) if "recordings" in bestResult and len(bestResult["recordings"]) > 0: ans = "mbid:{}".format(bestResult["recordings"][0]["id"]) else: ans = "acoustid:{}".format(bestResult["id"]) return ans
def logWarning(msg, xml): logging.warning( __name__, msg + '\n' + etree.tostring(xml, pretty_print=True, encoding=str))
def logWarning(msg, xml): logging.warning(__name__, msg + '\n' + etree.tostring(xml, pretty_print=True, encoding=str))