def test_tag_query_escaped_pipe(s): pat = Pattern(r'<albumartist=/Lee\|Bob/|matched|not matched>') s.assertEquals(pat.format(s.g), 'matched') pat = Pattern(r'<albumartist=\||matched|not matched>') s.assertEquals(pat.format(s.g), 'not matched') pat = Pattern(r'<comment=/Trouble\|Strife/|matched|not matched>') s.assertEquals(pat.format(s.g), 'matched')
def test_tag_query_regex(s): pat = Pattern("<album=/'only'/|matched|not matched>") s.assertEquals(pat.format(s.g), 'matched') pat = Pattern("<album=/The .+ way/|matched|not matched>") s.assertEquals(pat.format(s.g), 'matched') pat = Pattern("</The .+ way/|matched|not matched>") s.assertEquals(pat.format(s.g), 'not matched')
def test_same2(s): fpat = FileFromPattern('<~filename>') pat = Pattern('<~filename>') s.assertEquals(fpat.format_list(s.a), {(fpat.format(s.a), fpat.format(s.a))}) s.assertEquals(pat.format_list(s.a), {(pat.format(s.a), pat.format(s.a))})
def test_space(self): pat = Pattern("a ") self.assertEqual(pat.format(self.a), "a ") pat = Pattern(" a") self.assertEqual(pat.format(self.a), " a") pat = Pattern("a\n\n") self.assertEqual(pat.format(self.a), "a\n\n")
def test_same(s): pat = Pattern('<~basename> <title>') s.failUnlessEqual(pat.format_list(s.a), {(pat.format(s.a), pat.format(s.a))}) pat = Pattern('/a<genre|/<genre>>/<title>') s.failUnlessEqual(pat.format_list(s.a), {(pat.format(s.a), pat.format(s.a))})
def test_tag_internal(self): if os.name != "nt": pat = Pattern("<~filename='/path/to/a.mp3'|matched|not matched>") self.assertEquals(pat.format(self.a), 'matched') pat = Pattern( "<~filename=/\\/path\\/to\\/a.mp3/|matched|not matched>") self.assertEquals(pat.format(self.a), 'matched') else: pat = Pattern( r"<~filename='C:\\\path\\\to\\\a.mp3'|matched|not matched>") self.assertEquals(pat.format(self.a), 'matched')
def plugin_on_song_started(self, song): self.song = song pat_str = self.config_get(*Config.PAT_PLAYING) pattern = Pattern(pat_str) status = (pattern.format(song) if song else self.config_get(Config.STATUS_SONGLESS, "")) self._set_status(status)
def plugin_songs(self, songs): # Check this is a launch, not a configure if self.chosen_site: url_pat = self.get_url_pattern(self.chosen_site) pat = Pattern(url_pat) urls = set() for song in songs: # Generate a sanitised AudioFile; allow through most tags subs = AudioFile() for k in (USER_TAGS + MACHINE_TAGS): vals = song.comma(k) if vals: try: encoded = unicode(vals).encode('utf-8') subs[k] = (encoded if k == 'website' else quote_plus(encoded)) # Dodgy unicode problems except KeyError: print_d("Problem with %s tag values: %r" % (k, vals)) url = str(pat.format(subs)) if not url: print_w("Couldn't build URL using \"%s\"." "Check your pattern?" % url_pat) return # Grr, set.add() should return boolean... if url not in urls: urls.add(url) website(url)
def test_escape_slash(s): fpat = s._create('<~filename>') s.assertTrue(fpat.format(s.a).endswith("_path_to_a.mp3")) pat = Pattern('<~filename>') if os.name != "nt": s.assertTrue(pat.format(s.a).startswith("/path/to/a")) else: s.assertTrue(pat.format(s.a).startswith("C:\\path\\to\\a")) if os.name != "nt": wpat = s._create(r'\\<artist>\\ "<title>') s.assertTrue( wpat.format(s.a).startswith(r'\Artist\ "Title5')) else: # FIXME.. pass
def website_for(pat: Pattern, song: AudioFile) -> Optional[str]: """Gets a utf-8 encoded string for a website from the given pattern""" # Generate a sanitised AudioFile; allow through most tags subs = AudioFile() # See issue 2762 for k in (USER_TAGS + MACHINE_TAGS + ['~filename']): vals = song.comma(k) if vals: try: # Escaping ~filename stops ~dirname ~basename etc working # But not escaping means ? % & will cause problems. # Who knows what user wants to do with /, seems better raw. subs[k] = (vals if k in ['website', '~filename'] else quote_plus(vals)) except KeyError: print_d("Problem with %s tag values: %r" % (k, vals)) return pat.format(subs) or None
def test_conditional_equals(s): pat = Pattern('<artist=Artist|matched|not matched>') s.assertEquals(pat.format(s.a), 'matched') pat = Pattern('<artist=Artistic|matched|not matched>') s.assertEquals(pat.format(s.a), 'not matched')
def test_conditional_genre(s): pat = Pattern('<genre|<genre>|music>') s.assertEquals(pat.format(s.a), 'music') s.assertEquals(pat.format(s.b), 'music') s.assertEquals(pat.format(s.c), '/, /')
class Command(JSONObject): """ Wraps an arbitrary shell command and its argument pattern. Serialises as JSON for some editability """ NAME = _("Command") FIELDS = { "name": Field(_("name"), _("The name of this command")), "command": Field(_("command"), _("The shell command syntax to run")), "parameter": Field(_("parameter"), _("If specified, a parameter whose occurrences in " "the command will be substituted with a " "user-supplied value, e.g. by using 'PARAM' " "all instances of '{PARAM}' in your command will " "have the value prompted for when run")), "pattern": Field(_("pattern"), _("The QL pattern, e.g. <~filename>, to use to " "compute a value for the command. For playlists, " "this also supports virtual tags <~playlistname> " "and <~#playlistindex>.")), "unique": Field(_("unique"), _("If set, this will remove duplicate computed values " "of the pattern")), "max_args": Field(_("max args"), _("The maximum number of argument to pass to the " "command at one time (like xargs)")), } def __init__(self, name=None, command=None, pattern="<~filename>", unique=False, parameter=None, max_args=10000, warn_threshold=50): JSONObject.__init__(self, name) self.command = str(command or "") self.pattern = str(pattern) self.unique = bool(unique) self.max_args = max_args self.parameter = str(parameter or "") self.__pat = Pattern(self.pattern) self.warn_threshold = warn_threshold def run(self, songs, playlist_name=None): """ Runs this command on `songs`, splitting into multiple calls if necessary. `playlist_name` if populated contains the Playlist's name. """ args = [] template_vars = {} if self.parameter: value = GetStringDialog(None, _("Input value"), _("Value for %s?") % self.parameter).run() template_vars[self.parameter] = value if playlist_name: print_d("Playlist command for %s" % playlist_name) template_vars["PLAYLIST"] = playlist_name self.command = self.command.format(**template_vars) print_d("Actual command=%s" % self.command) for i, song in enumerate(songs): wrapped = SongWrapper(song) if playlist_name: wrapped["~playlistname"] = playlist_name wrapped["~playlistindex"] = str(i + 1) wrapped["~#playlistindex"] = i + 1 arg = str(self.__pat.format(wrapped)) if not arg: print_w("Couldn't build shell command using \"%s\"." "Check your pattern?" % self.pattern) break if not self.unique: args.append(arg) elif arg not in args: args.append(arg) max = int((self.max_args or 10000)) com_words = self.command.split(" ") while args: print_d("Running %s with %d substituted arg(s) (of %d%s total)..." % (self.command, min(max, len(args)), len(args), " unique" if self.unique else "")) util.spawn(com_words + args[:max]) args = args[max:] @property def playlists_only(self): return ("~playlistname" in self.pattern or "playlistindex" in self.pattern) def __str__(self): return 'Command: "{command} {pattern}"'.format(**dict(self.data))
def test_same2(s): fpat = FileFromPattern('<~filename>') pat = Pattern('<~filename>') s.assertEquals(fpat.format_list(s.a), set([fpat.format(s.a)])) s.assertEquals(pat.format_list(s.a), set([pat.format(s.a)]))
def test_query_like_tag(self): pat = Pattern("<t=v>") self.assertEqual(pat.format(AudioFile({"t=v": "foo"})), "foo")
def test_query_scope(self): pat = Pattern("<foo|<artist=Foo|x|y>|<artist=Foo|z|q>>") self.assertEqual(pat.format(self.f), "z")
class Command(JSONObject): """ Wraps an arbitrary shell command and its argument pattern. Serialises as JSON for some editability """ NAME = _("Command") FIELDS = { "name": Field(_("name"), _("The name of this command")), "command": Field(_("command"), _("The shell command syntax to run")), "parameter": Field(_("parameter"), _("If specified, a parameter whose occurrences in " "the command will be substituted with a " "user-supplied value, e.g. by using 'PARAM' " "all instances of '{PARAM}' in your command will " "have the value prompted for when run")), "pattern": Field(_("pattern"), _("The QL pattern, e.g. <~filename>, to use to " "compute a value for the command. For playlists, " "this also supports virtual tags <~playlistname> " "and <~#playlistindex>.")), "unique": Field(_("unique"), _("If set, this will remove duplicate computed values " "of the pattern")), "max_args": Field(_("max args"), _("The maximum number of argument to pass to the " "command at one time (like xargs)")), } def __init__(self, name=None, command=None, pattern="<~filename>", unique=False, parameter=None, max_args=10000, warn_threshold=50): JSONObject.__init__(self, name) self.command = str(command or "") self.pattern = str(pattern) self.unique = bool(unique) self.max_args = max_args self.parameter = str(parameter or "") self.__pat = Pattern(self.pattern) self.warn_threshold = warn_threshold def run(self, songs, playlist_name=None): """ Runs this command on `songs`, splitting into multiple calls if necessary. `playlist_name` if populated contains the Playlist's name. """ args = [] template_vars = {} if self.parameter: value = GetStringDialog(None, _("Input value"), _("Value for %s?") % self.parameter).run() template_vars[self.parameter] = value if playlist_name: print_d("Playlist command for %s" % playlist_name) template_vars["PLAYLIST"] = playlist_name actual_command = self.command.format(**template_vars) print_d("Actual command=%s" % actual_command) for i, song in enumerate(songs): wrapped = SongWrapper(song) if playlist_name: wrapped["~playlistname"] = playlist_name wrapped["~playlistindex"] = str(i + 1) wrapped["~#playlistindex"] = i + 1 arg = str(self.__pat.format(wrapped)) if not arg: print_w("Couldn't build shell command using \"%s\"." "Check your pattern?" % self.pattern) break if not self.unique: args.append(arg) elif arg not in args: args.append(arg) max = int((self.max_args or 10000)) com_words = actual_command.split(" ") while args: print_d("Running %s with %d substituted arg(s) (of %d%s total)..." % (actual_command, min(max, len(args)), len(args), " unique" if self.unique else "")) util.spawn(com_words + args[:max]) args = args[max:] @property def playlists_only(self): return ("~playlistname" in self.pattern or "playlistindex" in self.pattern) def __str__(self): return "{command} {pattern}".format(**dict(self.data))
def test_conditional_other_number_dot_title(s): pat = Pattern('<tracknumber|<tracknumber>|00>. <title>') s.assertEquals(pat.format(s.a), '5/6. Title5') s.assertEquals(pat.format(s.b), '6. Title6') s.assertEquals(pat.format(s.c), '00. test/subdir')
def test_duplicate_query(self): pat = Pattern('<u=yes|<u=yes|x|y>|<u=yes|q|z>>') self.assertEqual(pat.format(AudioFile({"u": u"yes"})), "x") self.assertEqual(pat.format(AudioFile({"u": u"no"})), "z")
def test_recnumber_dot_title(s): pat = Pattern(r'\<<tracknumber>\>. <title>') s.assertEquals(pat.format(s.a), '<5/6>. Title5') s.assertEquals(pat.format(s.b), '<6>. Title6') s.assertEquals(pat.format(s.c), '<>. test/subdir')
def test_tag_query_quoting(s): pat = Pattern('<album=The only way|matched|not matched>') s.assertEquals(pat.format(s.g), 'not matched') pat = Pattern("<album=\"The 'only' way!\"|matched|not matched>") s.assertEquals(pat.format(s.g), 'matched')
def test_number_dot_genre(s): pat = Pattern('<tracknumber>. <genre>') s.assertEquals(pat.format(s.a), '5/6. ') s.assertEquals(pat.format(s.b), '6. ') s.assertEquals(pat.format(s.c), '. /, /')
def test_conditional_notfile(s): pat = Pattern('<tracknumber|<tracknumber>|00>') s.assertEquals(pat.format(s.a), '5/6') s.assertEquals(pat.format(s.b), '6') s.assertEquals(pat.format(s.c), '00')
def test_escape(self): pat = Pattern("a \\<foo\\|bla\\>") self.assertEqual(pat.format(self.a), "a <foo|bla>") pat = Pattern(r"a\\<foo>") self.assertEqual(pat.format(self.a), "a\\")
def test_generated_and_not_generated(s): pat = Pattern('<~basename> <title>') res = pat.format(s.a) s.assertEquals( res, os.path.basename(s.a["~filename"]) + " " + s.a["title"])
def test_conditional_unknown(s): pat = Pattern('<album|foo|bar>') s.assertEquals(pat.format(s.a), 'bar')
def test_numeric(self): pat = Pattern("<~#rating>") self.assertEqual(pat.format(self.a), "0.50")
def test_conditional_equals_unicode(s): pat = Pattern(u'<artist=Artist|matched|not matched>') s.assertEquals(pat.format(s.g), 'not matched') pat = Pattern(u'<artist=un élève français|matched|not matched>') s.assertEquals(pat.format(s.g), 'matched')
def test_tag_internal(self): pat = Pattern("<~filename='/path/to/a.mp3'|matched|not matched>") self.assertEquals(pat.format(self.a), 'matched') pat = Pattern("<~filename=/\\/path\\/to\\/a.mp3/|matched|not matched>") self.assertEquals(pat.format(self.a), 'matched')
def test_same(s): pat = Pattern('<~basename> <title>') s.failUnlessEqual(pat.format_list(s.a), set([pat.format(s.a)])) pat = Pattern('/a<genre|/<genre>>/<title>') s.failUnlessEqual(pat.format_list(s.a), set([pat.format(s.a)]))
def test_duplicate_query(self): pat = Pattern('<u=yes|<u=yes|x|y>|<u=yes|q|z>>') self.assertEqual(pat.format(AudioFile({"u": "yes"})), "x") self.assertEqual(pat.format(AudioFile({"u": "no"})), "z")
def test_query_numeric(self): pat = Pattern("<#(foo=42)|42|other>") self.assertEqual(pat.format(AudioFile()), "other") self.assertEqual(pat.format(AudioFile({"foo": "42"})), "42")
def plugin_on_paused(self): pat_str = self.config_get(*Config.PAT_PAUSED) pattern = Pattern(pat_str) self.status = pattern.format(self.song) if self.song else "" self._set_status(self.status)
def test_conditional_subdir(s): pat = Pattern('/a<genre|/<genre>>/<title>') s.assertEquals(pat.format(s.a), '/a/Title5') s.assertEquals(pat.format(s.b), '/a/Title6') s.assertEquals(pat.format(s.c), '/a//, //test/subdir')
def test_recnumber_dot_title(s): pat = Pattern('\<<tracknumber>\>. <title>') s.assertEquals(pat.format(s.a), '<5/6>. Title5') s.assertEquals(pat.format(s.b), '<6>. Title6') s.assertEquals(pat.format(s.c), '<>. test/subdir')
def test_tag_query_escaping(s): pat = Pattern('<albumartist=Lee "Scratch" Perry|matched|not matched>') s.assertEquals(pat.format(s.g), 'matched')
def test_generated(s): pat = Pattern('<~basename>') s.assertEquals(pat.format(s.a), os.path.basename(s.a["~filename"]))
def test_tag_query_disallowed_free_text(s): pat = Pattern("<The only way|matched|not matched>") s.assertEquals(pat.format(s.g), 'not matched')
def test_number_dot_title_dot(s): pat = Pattern('<tracknumber>. <title>.') s.assertEquals(pat.format(s.a), '5/6. Title5.') s.assertEquals(pat.format(s.b), '6. Title6.') s.assertEquals(pat.format(s.c), '. test/subdir.')
def test_unicode_with_int(s): song = AudioFile({"tracknumber": "5/6", "title": "\xe3\x81\x99\xe3\x81\xbf\xe3\x82\x8c".decode('utf-8')}) pat = Pattern('<~#track>. <title>') s.assertEquals(pat.format(song), "5. \xe3\x81\x99\xe3\x81\xbf\xe3\x82\x8c".decode('utf-8'))
def test_unicode_with_int(s): song = AudioFile({"tracknumber": "5/6", "title": b"\xe3\x81\x99\xe3\x81\xbf\xe3\x82\x8c".decode('utf-8')}) pat = Pattern('<~#track>. <title>') s.assertEquals(pat.format(song), b"5. \xe3\x81\x99\xe3\x81\xbf\xe3\x82\x8c".decode('utf-8'))
class Command(JSONObject): """ Wraps an arbitrary shell command and its argument pattern. Serialises as JSON for some editability """ NAME = _("Command") FIELDS = { "name": Field(_("name"), _("The name of this command")), "command": Field(_("command"), _("The shell command syntax to run")), "parameter": Field(_("parameter"), _("If specified, a parameter whose occurrences in " "the command will be substituted with a " "user-supplied value, e.g. by using 'PARAM' " "all instances of '{PARAM}' in your command will " "have the value prompted for when run")), "pattern": Field(_("pattern"), _("The QL pattern, e.g. <~filename>, to use to " "compute a value for the command")), "unique": Field(_("unique"), _("If set, this will remove duplicate computed values " "of the pattern")), "max_args": Field(_("max args"), _("The maximum number of argument to pass to the " "command at one time (like xargs)")), } def __init__(self, name=None, command=None, pattern="<~filename>", unique=False, parameter=None, max_args=10000, warn_threshold=100): JSONObject.__init__(self, name) self.command = str(command or "") self.pattern = str(pattern) self.unique = bool(unique) self.max_args = max_args self.parameter = str(parameter or "") self.__pat = Pattern(self.pattern) self.warn_threshold = warn_threshold def run(self, songs): """ Runs this command on `songs`, splitting into multiple calls if necessary """ args = [] if self.parameter: value = GetStringDialog(None, _("Input value"), _("Value for %s?") % self.parameter).run() self.command = self.command.format(**{self.parameter: value}) print_d("Actual command=%s" % self.command) for song in songs: arg = str(self.__pat.format(song)) if not arg: print_w("Couldn't build shell command using \"%s\"." "Check your pattern?" % self.pattern) break if not self.unique: args.append(arg) elif arg not in args: args.append(arg) max = int((self.max_args or 10000)) com_words = self.command.split(" ") while args: print_d("Running %s with %d substituted arg(s) (of %d%s total)..." % (self.command, min(max, len(args)), len(args), " unique" if self.unique else "")) util.spawn(com_words + args[:max]) args = args[max:] def __str__(self): return "Command= {command} {pattern}".format(**dict(self.data))
class Command(JSONObject): """ Wraps an arbitrary shell command and its argument pattern. Serialises as JSON for some editability """ NAME = _("Command") FIELDS = { "name": Field(_("name"), _("The name of this command")), "command": Field(_("command"), _("The shell command syntax to run")), "parameter": Field( _("parameter"), _("If specified, a parameter whose occurrences in " "the command will be substituted with a " "user-supplied value, e.g. by using 'PARAM' " "all instances of '{PARAM}' in your command will " "have the value prompted for when run")), "pattern": Field( _("pattern"), _("The QL pattern, e.g. <~filename>, to use to " "compute a value for the command")), "unique": Field( _("unique"), _("If set, this will remove duplicate computed values " "of the pattern")), "max_args": Field( _("max args"), _("The maximum number of argument to pass to the " "command at one time (like xargs)")), } def __init__(self, name=None, command=None, pattern="<~filename>", unique=False, parameter=None, max_args=10000, warn_threshold=100): JSONObject.__init__(self, name) self.command = str(command or "") self.pattern = str(pattern) self.unique = bool(unique) self.max_args = max_args self.parameter = str(parameter or "") self.__pat = Pattern(self.pattern) self.warn_threshold = warn_threshold def run(self, songs): """ Runs this command on `songs`, splitting into multiple calls if necessary """ args = [] if self.parameter: value = GetStringDialog(None, _("Input value"), _("Value for %s?") % self.parameter).run() self.command = self.command.format(**{self.parameter: value}) print_d("Actual command=%s" % self.command) for song in songs: arg = str(self.__pat.format(song)) if not arg: print_w("Couldn't build shell command using \"%s\"." "Check your pattern?" % self.pattern) break if not self.unique: args.append(arg) elif arg not in args: args.append(arg) max = int((self.max_args or 10000)) com_words = self.command.split(" ") while args: print_d( "Running %s with %d substituted arg(s) (of %d%s total)..." % (self.command, min(max, len(args)), len(args), " unique" if self.unique else "")) util.spawn(com_words + args[:max]) args = args[max:] def __str__(self): return "Command= {command} {pattern}".format(**dict(self.data))