def test_build_queries(self): spl = SmartPlaylistPlugin() self.assertEqual(spl._matched_playlists, None) self.assertEqual(spl._unmatched_playlists, None) config['smartplaylist']['playlists'].set([]) spl.build_queries() self.assertEqual(spl._matched_playlists, set()) self.assertEqual(spl._unmatched_playlists, set()) config['smartplaylist']['playlists'].set([ {'name': 'foo', 'query': 'FOO foo'}, {'name': 'bar', 'album_query': ['BAR bar1', 'BAR bar2']}, {'name': 'baz', 'query': 'BAZ baz', 'album_query': 'BAZ baz'} ]) spl.build_queries() self.assertEqual(spl._matched_playlists, set()) foo_foo = parse_query_string('FOO foo', Item) baz_baz = parse_query_string('BAZ baz', Item) baz_baz2 = parse_query_string('BAZ baz', Album) bar_bar = OrQuery((parse_query_string('BAR bar1', Album)[0], parse_query_string('BAR bar2', Album)[0])) self.assertEqual(spl._unmatched_playlists, set([ ('foo', foo_foo, (None, None)), ('baz', baz_baz, baz_baz2), ('bar', (None, None), (bar_bar, None)), ]))
def test_build_queries(self): spl = SmartPlaylistPlugin() self.assertEqual(spl._matched_playlists, None) self.assertEqual(spl._unmatched_playlists, None) config['smartplaylist']['playlists'].set([]) spl.build_queries() self.assertEqual(spl._matched_playlists, set()) self.assertEqual(spl._unmatched_playlists, set()) config['smartplaylist']['playlists'].set([ {'name': u'foo', 'query': u'FOO foo'}, {'name': u'bar', 'album_query': [u'BAR bar1', u'BAR bar2']}, {'name': u'baz', 'query': u'BAZ baz', 'album_query': u'BAZ baz'} ]) spl.build_queries() self.assertEqual(spl._matched_playlists, set()) foo_foo = parse_query_string(u'FOO foo', Item) baz_baz = parse_query_string(u'BAZ baz', Item) baz_baz2 = parse_query_string(u'BAZ baz', Album) bar_bar = OrQuery((parse_query_string(u'BAR bar1', Album)[0], parse_query_string(u'BAR bar2', Album)[0])) self.assertEqual(spl._unmatched_playlists, set([ (u'foo', foo_foo, (None, None)), (u'baz', baz_baz, baz_baz2), (u'bar', (None, None), (bar_bar, None)), ]))
def list_sole_tracks(self, lib): check_single_track = self.config["check_single_track"].get(True) artist_fields = self.config["artist_fields"].as_str_seq() check_fields = self.config["check_fields"].as_str_seq() sections = self.config["sections"].as_str_seq() base_check_query = self.config["check_query"].as_str() self._log.debug(f"Using check query {base_check_query}") base_check_query, _ = library.parse_query_string( base_check_query, library.Item) candidates_by_section = collections.defaultdict(set) checked_by_artist = collections.defaultdict( lambda: collections.defaultdict(set)) self._log.debug("Gathering item artist information") for item in lib.items(): base_match = self.base_query.match(item) check_match = base_check_query.match(item) if not base_match and not check_match: continue for section in sections: section_query, _ = library.parse_query_string( section, library.Item) if section_query.match(item): if base_match: candidates_by_section[section].add(item) if check_match: for artist in self.artists(item, check_fields): checked_by_artist[section][artist].add(item) seen = set() for section in sections: self._log.info(f"Checking section: {section}") for item in candidates_by_section[section]: if item in seen: continue seen.add(item) if check_single_track: if item.album and item._cached_album: if len(item._cached_album.items()) > 1: # Not a single track continue item_artists = self.artists(item, artist_fields) self._log.debug( f'Checking for artists ({", ".join(item_artists)})') for artist in item_artists: other_items = [ i for i in checked_by_artist[section][artist] if i != item ] if other_items: yield item, False break else: yield item, True
def build_queries(self): """ Instanciate queries for the alternatives. Each Alternate has 2 queries, one for items and one for albums. We must also remember its name. _unmatched_playlists is a set of tuples (name, (q, q_sort), (album_q, album_q_sort)). """ self._unmatched_alternatives = set() self._matched_alternatives = set() for alternative in self.config['alternatives'].get(list): if 'name' not in alternative: self._log.warning("Alternative configuration is missing name") continue alt_data = (alternative['name'], ) try: for key, Model, in (('query', Item), ('album_query', Album)): qs = alternative.get(key) if qs is None: query_and_sort = None, None elif isinstance(qs, six.string_types): query_and_sort = parse_query_string(qs, Model) elif len(qs) == 1: query_and_sort = parse_query_string(qs[0], Model) else: # multiple queries and sorts queries, sorts = zip(*(parse_query_string(q, Model) for q in qs)) query = OrQuery(queries) final_sorts = [] for s in sorts: if s: if isinstance(s, MultipleSort): final_sorts += s.sorts else: final_sorts.append(s) if not final_sorts: sort = None elif len(final_sorts) == 1: sort, = final_sorts else: sort = MultipleSort(final_sorts) query_and_sort = query, sort alt_data += (query_and_sort, ) except Exception as exc: self._log.warning("invalid query in alternative {}: {}", alternative['name'], exc) continue self._unmatched_alternatives.add(alt_data)
def build_queries(self): """ Instanciate queries for the playlists. Each playlist has 2 queries: one or items one for albums, each with a sort. We must also remember its name. _unmatched_playlists is a set of tuples (name, (q, q_sort), (album_q, album_q_sort)). sort may be any sort, or NullSort, or None. None and NullSort are equivalent and both eval to False. More precisely - it will be NullSort when a playlist query ('query' or 'album_query') is a single item or a list with 1 element - it will be None when there are multiple items i a query """ self._unmatched_playlists = set() self._matched_playlists = set() for playlist in self.config['playlists'].get(list): playlist_data = (playlist['name'],) for key, Model in (('query', Item), ('album_query', Album)): qs = playlist.get(key) if qs is None: query_and_sort = None, None elif isinstance(qs, basestring): query_and_sort = parse_query_string(qs, Model) elif len(qs) == 1: query_and_sort = parse_query_string(qs[0], Model) else: # multiple queries and sorts queries, sorts = zip(*(parse_query_string(q, Model) for q in qs)) query = OrQuery(queries) final_sorts = [] for s in sorts: if s: if isinstance(s, MultipleSort): final_sorts += s.sorts else: final_sorts.append(s) if not final_sorts: sort = None elif len(final_sorts) == 1: sort, = final_sorts else: sort = MultipleSort(final_sorts) query_and_sort = query, sort playlist_data += (query_and_sort,) self._unmatched_playlists.add(playlist_data)
def build_queries(self): """ Instanciate queries for the playlists. Each playlist has 2 queries: one or items one for albums, each with a sort. We must also remember its name. _unmatched_playlists is a set of tuples (name, (q, q_sort), (album_q, album_q_sort)). sort may be any sort, or NullSort, or None. None and NullSort are equivalent and both eval to False. More precisely - it will be NullSort when a playlist query ('query' or 'album_query') is a single item or a list with 1 element - it will be None when there are multiple items i a query """ self._unmatched_playlists = set() self._matched_playlists = set() for playlist in self.config['playlists'].get(list): playlist_data = (playlist['name'], ) for key, Model in (('query', Item), ('album_query', Album)): qs = playlist.get(key) if qs is None: query_and_sort = None, None elif isinstance(qs, basestring): query_and_sort = parse_query_string(qs, Model) elif len(qs) == 1: query_and_sort = parse_query_string(qs[0], Model) else: # multiple queries and sorts queries, sorts = zip(*(parse_query_string(q, Model) for q in qs)) query = OrQuery(queries) final_sorts = [] for s in sorts: if s: if isinstance(s, MultipleSort): final_sorts += s.sorts else: final_sorts.append(s) if not final_sorts: sort = None elif len(final_sorts) == 1: sort, = final_sorts else: sort = MultipleSort(final_sorts) query_and_sort = query, sort playlist_data += (query_and_sort, ) self._unmatched_playlists.add(playlist_data)
def find_items_to_analyse(self): # Parse the incoming query parsed_query, parsed_sort = parse_query_string(" ".join(self.query), Item) combined_query = parsed_query # Add unprocessed items query if not self.cfg_force: # Set up the query for unprocessed items subqueries = [] target_maps = ["low_level_targets", "high_level_targets"] for map_key in target_maps: target_map = self.config[map_key] for fld in target_map: if target_map[fld]["required"].exists( ) and target_map[fld]["required"].get(bool): fast = fld in Item._fields query_item = dbcore.query.MatchQuery(fld, None, fast=fast) subqueries.append(query_item) unprocessed_items_query = dbcore.query.OrQuery(subqueries) combined_query = dbcore.query.AndQuery( [parsed_query, unprocessed_items_query]) self._say("Combined query: {}".format(combined_query)) # Get the library items self.items_to_analyse = self.lib.items(combined_query, parsed_sort) if len(self.items_to_analyse) == 0: self._say("No items to process") return
def _items_for_query(lib, playlist, album=False): """Get the matching items for a playlist's configured queries. `album` indicates whether to process the item-level query or the album-level query (if any). """ key = 'album_query' if album else 'query' if key not in playlist: return [] # Parse quer(ies). If it's a list, join the queries with OR. query_strings = playlist[key] if not isinstance(query_strings, (list, tuple)): query_strings = [query_strings] model = library.Album if album else library.Item query = dbcore.OrQuery( [library.parse_query_string(q, model)[0] for q in query_strings]) # Execute query, depending on type. if album: result = [] for album in lib.albums(query): result.extend(album.items()) return result else: return lib.items(query)
def create(self, lib: Library, name: str, qs: str, playlist_dir: str, relative_to: str): if playlist_dir is None: playlist_dir = self.config_playlist_dir() if relative_to is None: relative_to = self.config_relative_to() # Try to parse the query try: if qs is None: query, sort = None, None else: query, sort = parse_query_string(qs, Item) except ParsingError as ex: self._log.warning(u'invalid query: {}', ex) return # Map items to their paths items = lib.items(query, sort) item_path: Callable[[Item], str] = lambda item: path.relpath( item.path.decode('utf-8'), relative_to) paths = [item_path(item) for item in items] filename = path.join(playlist_dir, name + '.m3u') with open(filename, 'w+') as file: write_str = '\n'.join(paths) file.write(write_str)
def __init__(self, query_string): self.query_string = query_string if self.query_string in self._cached_results: albums = self.albums = self._cached_results[self.query_string] else: query, _ = parse_query_string(query_string, Item) album_only_query = AndQuery( [query, NotQuery(MatchQuery(u'album_id', 0))]) items = self.lib.items(album_only_query) def item_album(i): return i.album_id items_by_album = sorted(items, key=item_album) grouped_items = groupby(items_by_album, item_album) albums = set() for album_id, items in grouped_items: items = list(items) album = items[0]._cached_album() all_items = album.items() if len(items) == len(all_items): albums.add(album_id) self._cached_results[self.query_string] = self.albums = albums self.album_query = OrQuery([MatchQuery('id', id) for id in albums])
def parse_item_query(self, name): if name not in config['item_queries']: # Fall back to album if name in config['album_queries']: return self.parse_album_query(name) querystring = config['item_queries'][name].as_str() query, _ = parse_query_string(querystring, Item) return query
def do_i_hate_this(cls, task, action_patterns): """Process group of patterns (warn or skip) and returns True if task is hated and not whitelisted. """ if action_patterns: for query_string in action_patterns: query, _ = parse_query_string(query_string, Album if task.is_album else Item) if any(query.match(item) for item in task.imported_items()): return True return False
def __init__(self, query_string): self.query_string = query_string if self.query_string in self._cached_results: albums = self.albums = self._cached_results[self.query_string] else: query, _ = parse_query_string(query_string, Item) album_only_query = AndQuery( [query, NotQuery(MatchQuery(u'album_id', 0))]) items = self.lib.items(album_only_query) albums = set(i.album_id for i in items) self._cached_results[self.query_string] = self.albums = albums self.album_query = OrQuery([MatchQuery('id', id) for id in albums])
def handle_common_args(self, opts, args): self.config.set_args(opts) query = ui.decargs(args) if query: self.config["query"] = query self._log.debug(f'Using base query {" ".join(query)}') base_query, _ = library.parse_query_parts(query, library.Item) else: query = self.config["query"].as_str() self._log.debug(f"Using base query {query}") base_query, _ = library.parse_query_string(query, library.Item) self.base_query = base_query
def __init__(self, query_string): self.query_string = query_string if self.query_string in self._cached_album_results: albums = self.albums = self._cached_album_results[ self.query_string] else: query, _ = parse_query_string(query_string, Album) albums = self.lib.albums(query) self._cached_album_results[ self.query_string] = self.albums = albums self.item_query = OrQuery( [MatchQuery('album_id', album.id) for album in albums])
def get_modifies(self, items, model_cls, context): modifies = [] for query, modify in items: modify = modify.as_str() mod_query, mods, dels = self.parse_modify(modify, model_cls) if mod_query: raise ui.UserError(u'modifyonimport.{0}["{1}"]: unexpected query `{2}` in value'.format(context, query, mod_query)) elif not mods and not dels: raise ui.UserError(u'modifyonimport.{0}["{1}"]: no modifications found'.format(context, query)) dbquery, _ = parse_query_string(util.as_string(query), model_cls) modifies.append((dbquery, mods, dels)) return modifies
def do_i_hate_this(cls, task, action_patterns): """Process group of patterns (warn or skip) and returns True if task is hated and not whitelisted. """ if action_patterns: for query_string in action_patterns: query, _ = parse_query_string( query_string, Album if task.is_album else Item, ) if any(query.match(item) for item in task.imported_items()): return True return False
def import_begin(self, session): self.should_write = ui.should_write() self.should_move = ui.should_move() for name, model_cls in [('album', Album), ('singleton', Item)]: modifies = self.get_modifies(self.config['modify_' + name].items(), model_cls, 'modify_' + name) setattr(self, name + '_modifies', modifies) self.album_item_modifies = [] for albumquery, itemmodifies in self.config['modify_album_items'].items(): albumdbquery, _ = parse_query_string(util.as_string(albumquery), Album) modifies = self.get_modifies(itemmodifies.items(), Item, u'modify_album_items.{0}'.format(albumquery)) self.album_item_modifies.append((albumdbquery, modifies))
def should_transcode(item, fmt): """Determine whether the item should be transcoded as part of conversion (i.e., its bitrate is high or it has the wrong format). """ no_convert_queries = config['convert']['no_convert'].as_str_seq() if no_convert_queries: for query_string in no_convert_queries: query, _ = parse_query_string(query_string, Item) if query.match(item): return False if config['convert']['never_convert_lossy_files'] and \ not (item.format.lower() in LOSSLESS_FORMATS): return False maxbr = config['convert']['max_bitrate'].get(int) return fmt.lower() != item.format.lower() or item.bitrate >= 1000 * maxbr
def should_transcode(item, fmt): """Determine whether the item should be transcoded as part of conversion (i.e., its bitrate is high or it has the wrong format). """ no_convert_queries = config['convert']['no_convert'].as_str_seq() if no_convert_queries: for query_string in no_convert_queries: query, _ = parse_query_string(query_string, Item) if query.match(item): return False if config['convert']['never_convert_lossy_files'] and \ not (item.format.lower() in LOSSLESS_FORMATS): return False maxbr = config['convert']['max_bitrate'].get(int) return fmt.lower() != item.format.lower() or \ item.bitrate >= 1000 * maxbr
def parse_config(self, config): if 'paths' in config: path_config = config['paths'] else: path_config = beets.config['paths'] self.path_formats = get_path_formats(path_config) self.query, _ = parse_query_string(config['query'].get(unicode), Item) self.removable = config.get(dict).get('removable', True) if 'directory' in config: dir = config['directory'].get(str) else: dir = self.name if not os.path.isabs(dir): dir = os.path.join(self.lib.directory, dir) self.directory = bytestring_path(dir)
def parse_config(self, config): if 'paths' in config: path_config = config['paths'] else: path_config = beets.config['paths'] self.path_formats = get_path_formats(path_config) query = get_unicode_config(config, 'query') self.query, _ = parse_query_string(query, Item) self.removable = config.get(dict).get('removable', True) if 'directory' in config: dir = config['directory'].get(str) else: dir = self.name if not os.path.isabs(dir): dir = os.path.join(self.lib.directory, dir) self.directory = bytestring_path(dir)
def parse_config(self, config): if 'paths' in config: path_config = config['paths'] else: path_config = beets.config['paths'] self.path_formats = get_path_formats(path_config) query = config['query'].as_str() self.query, _ = parse_query_string(query, Item) self.removable = config.get(dict).get('removable', True) self.copy_album_art = config.get(dict).get('copy_album_art', False) if 'directory' in config: dir = config['directory'].as_str() else: dir = self.name dir = bytestring_path(dir) if not os.path.isabs(syspath(dir)): dir = os.path.join(self.lib.directory, dir) self.directory = dir
def _items_for_query(lib, playlist, album=False): """Get the matching items for a playlist's configured queries. `album` indicates whether to process the item-level query or the album-level query (if any). """ key = 'album_query' if album else 'query' if key not in playlist: return [] # Parse quer(ies). If it's a list, perform the queries and manually # concatenate the results query_strings = playlist[key] if not isinstance(query_strings, (list, tuple)): query_strings = [query_strings] model = library.Album if album else library.Item results = [] for q in query_strings: query, sort = library.parse_query_string(q, model) if album: new = lib.albums(query, sort) else: new = lib.items(query, sort) results.extend(new) return results
def parse_album_query(self, name): querystring = config['album_queries'][name].as_str() query, _ = parse_query_string(querystring, Album) return query
def build_queries(self): """ Instantiate queries for the playlists. Each playlist has 2 queries: one or items one for albums, each with a sort. We must also remember its name. _unmatched_playlists is a set of tuples (name, (q, q_sort), (album_q, album_q_sort)). sort may be any sort, or NullSort, or None. None and NullSort are equivalent and both eval to False. More precisely - it will be NullSort when a playlist query ('query' or 'album_query') is a single item or a list with 1 element - it will be None when there are multiple items i a query """ self._unmatched_playlists = set() self._matched_playlists = set() for playlist in self.config['playlists'].get(list): if 'name' not in playlist: self._log.warning(u"playlist configuration is missing name") continue playlist_data = (playlist['name'], ) try: for key, model_cls in (('query', Item), ('album_query', Album)): qs = playlist.get(key) if qs is None: query_and_sort = None, None elif isinstance(qs, six.string_types): query_and_sort = parse_query_string(qs, model_cls) elif len(qs) == 1: query_and_sort = parse_query_string(qs[0], model_cls) else: # multiple queries and sorts queries, sorts = zip( *(parse_query_string(q, model_cls) for q in qs)) query = OrQuery(queries) final_sorts = [] for s in sorts: if s: if isinstance(s, MultipleSort): final_sorts += s.sorts else: final_sorts.append(s) if not final_sorts: sort = None elif len(final_sorts) == 1: sort, = final_sorts else: sort = MultipleSort(final_sorts) query_and_sort = query, sort playlist_data += (query_and_sort, ) except ParsingError as exc: self._log.warning(u"invalid query in playlist {}: {}", playlist['name'], exc) continue self._unmatched_playlists.add(playlist_data)