Skip to content

Commit 4f42ab1

Browse files
committed
fix url_encode aggressively encoding brackets
1 parent 063ec95 commit 4f42ab1

File tree

6 files changed

+92
-27
lines changed

6 files changed

+92
-27
lines changed

library/utils/arggroups.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -666,9 +666,31 @@ def sql_fs_post(args, table_prefix="m.") -> None:
666666

667667
if not args.no_url_encode_search:
668668
from library.utils.web import url_encode
669-
670-
args.include = [url_encode(s) if s.startswith("http") else s for s in args.include]
671-
args.exclude = [url_encode(s) if s.startswith("http") else s for s in args.exclude]
669+
from library.utils.path_utils import safe_unquote
670+
671+
new_include = []
672+
for s in args.include:
673+
if s.startswith("http"):
674+
variants = {s, url_encode(s), safe_unquote(s)}
675+
if len(variants) > 1:
676+
new_include.append(list(variants))
677+
else:
678+
new_include.append(s)
679+
else:
680+
new_include.append(s)
681+
args.include = new_include
682+
683+
new_exclude = []
684+
for s in args.exclude:
685+
if s.startswith("http"):
686+
variants = {s, url_encode(s), safe_unquote(s)}
687+
if len(variants) > 1:
688+
new_exclude.append(list(variants))
689+
else:
690+
new_exclude.append(s)
691+
else:
692+
new_exclude.append(s)
693+
args.exclude = new_exclude
672694

673695
parse_args_limit(args)
674696

library/utils/filter_engine.py

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -213,27 +213,47 @@ def construct_search_bindings(include, exclude, columns, exact=False, flexible_s
213213
sql = []
214214
bindings = {}
215215

216-
incl = ":" + param_key + "include{0}"
217-
includes = "(" + " OR ".join([f"{col} LIKE {incl}" for col in columns]) + ")"
218-
includes_sql_parts = []
219-
for idx, inc in enumerate(include):
220-
includes_sql_parts.append(includes.format(idx))
216+
def get_include_sql(idx_str, inc):
217+
incl = ":" + param_key + f"include{idx_str}"
221218
if exact:
222-
bindings[f"{param_key}include{idx}"] = inc
219+
bindings[f"{param_key}include{idx_str}"] = inc
220+
else:
221+
bindings[f"{param_key}include{idx_str}"] = "%" + inc.replace(" ", "%").replace("%%", " ") + "%"
222+
return "(" + " OR ".join([f"{col} LIKE {incl}" for col in columns]) + ")"
223+
224+
include_sql_parts = []
225+
for idx, inc in enumerate(include):
226+
if isinstance(inc, list):
227+
group_parts = [get_include_sql(f"{idx}_{sub_idx}", sub_inc) for sub_idx, sub_inc in enumerate(inc)]
228+
include_sql_parts.append("(" + " OR ".join(group_parts) + ")")
223229
else:
224-
bindings[f"{param_key}include{idx}"] = "%" + inc.replace(" ", "%").replace("%%", " ") + "%"
225-
join_op = " OR " if flexible_search else " AND "
226-
if len(includes_sql_parts) > 0:
227-
sql.append("AND (" + join_op.join(includes_sql_parts) + ")")
230+
include_sql_parts.append(get_include_sql(idx, inc))
231+
232+
if flexible_search:
233+
if include_sql_parts:
234+
sql.append("AND (" + " OR ".join(include_sql_parts) + ")")
235+
else:
236+
for part in include_sql_parts:
237+
sql.append("AND " + part)
228238

229239
excl = ":" + param_key + "exclude{0}"
230240
excludes = "AND (" + " AND ".join([f"COALESCE({col},'') NOT LIKE {excl}" for col in columns]) + ")"
231241
for idx, exc in enumerate(exclude):
232-
sql.append(excludes.format(idx))
233-
if exact:
234-
bindings[f"{param_key}exclude{idx}"] = exc
242+
if isinstance(exc, list):
243+
# For exclusion, OR inside the group means if ANY variant matches, it's excluded
244+
# This is equivalent to AND NOT variant1 AND NOT variant2
245+
for sub_idx, sub_exc in enumerate(exc):
246+
sub_idx_str = f"{idx}_{sub_idx}"
247+
bindings[f"{param_key}exclude{sub_idx_str}"] = (
248+
sub_exc if exact else "%" + sub_exc.replace(" ", "%").replace("%%", " ") + "%"
249+
)
250+
sql.append("AND (" + " AND ".join([f"COALESCE({col},'') NOT LIKE :{param_key}exclude{sub_idx_str}" for col in columns]) + ")")
235251
else:
236-
bindings[f"{param_key}exclude{idx}"] = "%" + exc.replace(" ", "%").replace("%%", " ") + "%"
252+
sql.append(excludes.format(idx))
253+
if exact:
254+
bindings[f"{param_key}exclude{idx}"] = exc
255+
else:
256+
bindings[f"{param_key}exclude{idx}"] = "%" + exc.replace(" ", "%").replace("%%", " ") + "%"
237257

238258
return sql, bindings
239259

library/utils/web.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -838,7 +838,7 @@ def safe_quote(url):
838838

839839
def selective_quote(component, restricted_chars):
840840
try:
841-
quoted = quote(component, safe="/:[]@!$&'()*+,;=", errors="strict")
841+
quoted = quote(component, errors="strict")
842842
except UnicodeDecodeError:
843843
return component
844844
return "".join(quote(char, safe="%") if char in restricted_chars else char for char in quoted)

tests/playback/test_links_open.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,33 @@ def test_links_open(mock_souffle):
2424
def test_links_open_brackets(mock_play, mock_souffle, temp_db):
2525
db_path = temp_db()
2626
url = "https://example.com/test?page[]=107"
27+
encoded_url = "https://example.com/test?page%5B%5D=107"
28+
29+
# 1. Add the link RAW
2730
lb(["links-add", "--no-extract", db_path, url])
28-
lb(["links-open", db_path, "-s", url])
2931

30-
assert mock_souffle.called
32+
# 2. Search for it with RAW URL (should find it because it's in include as-is)
33+
lb(["links-open", db_path, "-s", url])
34+
media = mock_souffle.call_args[0][1]
35+
assert len(media) == 1
36+
assert media[0]["path"] == url
37+
38+
# 3. Search for it with ENCODED URL (should find it because it's added as group [encoded, raw])
39+
lb(["links-open", db_path, "-s", encoded_url])
3140
media = mock_souffle.call_args[0][1]
3241
assert len(media) == 1
3342
assert media[0]["path"] == url
43+
44+
# 4. Clear and add it ENCODED
45+
import sqlite3
46+
conn = sqlite3.connect(db_path)
47+
conn.execute("DELETE FROM media")
48+
conn.commit()
49+
conn.close()
50+
lb(["links-add", "--no-extract", db_path, encoded_url])
51+
52+
# 5. Search for it with RAW URL (should find it because it matches the encoded version which is added to include)
53+
lb(["links-open", db_path, "-s", url])
54+
media = mock_souffle.call_args[0][1]
55+
assert len(media) == 1
56+
assert media[0]["path"] == encoded_url

tests/playback/test_play_actions.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@
2020
("--downloaded-before '1 day'", 0, ""),
2121
("--limit 1", 1, "corrupt.mp4"),
2222
("-L 1", 1, "corrupt.mp4"),
23-
("--online-media-only", 1, "https://test/?tags[]="),
24-
("--no-fts -s https://test/?tags[]=", 1, "https://test/?tags[]="),
25-
("--no-fts -s https://test/?tags%5B%5D=", 1, "https://test/?tags[]="),
26-
("--no-url-encode-search --no-fts -s https://test/?tags[]=", 1, "https://test/?tags[]="),
27-
("--no-url-encode-search --no-fts -s https://test/?tags%5B%5D=", 0, ""),
23+
("--online-media-only", 1, "https://test/?tags%5B%5D="),
24+
("--no-fts -s https://test/?tags%5B%5D=", 1, "https://test/?tags%5B%5D="),
25+
("--no-fts -s https://test/?tags[]=", 1, "https://test/?tags%5B%5D="),
26+
("--no-url-encode-search --no-fts -s https://test/?tags%5B%5D=", 1, "https://test/?tags%5B%5D="),
27+
("--no-url-encode-search --no-fts -s https://test/?tags[]=", 0, ""),
2828
("--offset 1", 4, "test.mp4"),
2929
("-s tests -s 'tests AND data' -E 2 -s test -E 3", 4, "corrupt.mp4"),
3030
("--created-within '30 years'", 5, "corrupt.mp4"),
@@ -83,7 +83,7 @@
8383
("-O duration", 5, "test.gif"),
8484
("-O locale_duration", 5, "test.gif"),
8585
("-O locale_size", 5, "test_frame.gif"),
86-
("-O reverse_path_path", 5, "https://test/?tags[]="),
86+
("-O reverse_path_path", 5, "https://test/?tags%5B%5D="),
8787
("-O size", 5, "test_frame.gif"),
8888
("-w time_deleted=0", 5, "corrupt.mp4"),
8989
]

tests/utils/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def get_default_args(*funcs):
7070
v_db = p("tests/data/video.db")
7171
if not Path(v_db).exists():
7272
lb(["fs-add", v_db, "--scan-subtitles", p("tests/data/"), "-E", "Youtube"])
73-
lb(["links-db", v_db, "--insert-only", "https://test/?tags[]="])
73+
lb(["links-db", v_db, "--insert-only", "https://test/?tags%5B%5D="])
7474

7575

7676
tube_db = p("tests/data/tube.db")

0 commit comments

Comments
 (0)