Skip to content

Commit 7c5c0d7

Browse files
committed
fix url_encode aggressively encoding brackets
1 parent 063ec95 commit 7c5c0d7

File tree

6 files changed

+97
-26
lines changed

6 files changed

+97
-26
lines changed

library/utils/arggroups.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -665,10 +665,32 @@ def sql_fs_post(args, table_prefix="m.") -> None:
665665
args.include[0] = shell_utils.resolve_absolute_path(args.include[0])
666666

667667
if not args.no_url_encode_search:
668+
from library.utils.path_utils import safe_unquote
668669
from library.utils.web import url_encode
669670

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]
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: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -213,27 +213,51 @@ 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(
251+
"AND ("
252+
+ " AND ".join([f"COALESCE({col},'') NOT LIKE :{param_key}exclude{sub_idx_str}" for col in columns])
253+
+ ")"
254+
)
235255
else:
236-
bindings[f"{param_key}exclude{idx}"] = "%" + exc.replace(" ", "%").replace("%%", " ") + "%"
256+
sql.append(excludes.format(idx))
257+
if exact:
258+
bindings[f"{param_key}exclude{idx}"] = exc
259+
else:
260+
bindings[f"{param_key}exclude{idx}"] = "%" + exc.replace(" ", "%").replace("%%", " ") + "%"
237261

238262
return sql, bindings
239263

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: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,40 @@ def test_links_open(mock_souffle):
1919
{"path": "https://site2", "hostname": "site2", "category": "p1"},
2020
]
2121

22+
2223
@mock.patch("library.playback.links_open.make_souffle")
2324
@mock.patch("library.playback.links_open.play")
2425
def test_links_open_brackets(mock_play, mock_souffle, temp_db):
2526
db_path = temp_db()
2627
url = "https://example.com/test?page[]=107"
28+
encoded_url = "https://example.com/test?page%5B%5D=107"
29+
30+
# 1. Add the link RAW
2731
lb(["links-add", "--no-extract", db_path, url])
32+
33+
# 2. Search for it with RAW URL (should find it because it's in include as-is)
2834
lb(["links-open", db_path, "-s", url])
29-
30-
assert mock_souffle.called
3135
media = mock_souffle.call_args[0][1]
3236
assert len(media) == 1
3337
assert media[0]["path"] == url
38+
39+
# 3. Search for it with ENCODED URL (should find it because it's added as group [encoded, raw])
40+
lb(["links-open", db_path, "-s", encoded_url])
41+
media = mock_souffle.call_args[0][1]
42+
assert len(media) == 1
43+
assert media[0]["path"] == url
44+
45+
# 4. Clear and add it ENCODED
46+
import sqlite3
47+
48+
conn = sqlite3.connect(db_path)
49+
conn.execute("DELETE FROM media")
50+
conn.commit()
51+
conn.close()
52+
lb(["links-add", "--no-extract", db_path, encoded_url])
53+
54+
# 5. Search for it with RAW URL (should find it because it matches the encoded version which is added to include)
55+
lb(["links-open", db_path, "-s", url])
56+
media = mock_souffle.call_args[0][1]
57+
assert len(media) == 1
58+
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)