Fix bugs, security issues and performance problems#404
Open
KAMI911 wants to merge 18 commits intolinuxmint:masterfrom
Open
Fix bugs, security issues and performance problems#404KAMI911 wants to merge 18 commits intolinuxmint:masterfrom
KAMI911 wants to merge 18 commits intolinuxmint:masterfrom
Conversation
os.system("mkdir -p '%s'" % PROVIDERS_PATH) is a shell injection risk:
if PROVIDERS_PATH ever contains shell metacharacters the shell would
execute arbitrary commands. Replace with os.makedirs(exist_ok=True)
which is both safer and avoids spawning a shell process.
provider_info.split(":::") with no bounds check raises a confusing
ValueError/TypeError on malformed data and makes it impossible to
distinguish corrupt settings from other errors. Count the fields
first and raise a descriptive ValueError so the caller (reload())
can log and skip the entry cleanly.
downloaded_bytes was incremented by the fixed block_bytes constant
(4 MB) on every iteration rather than by the actual number of bytes
received. The final size check therefore almost always reported an
incorrect file size and deleted the freshly-downloaded playlist.
Fix by incrementing by len(data) for byte chunks and by
len(data.encode('utf-8')) for already-decoded string chunks.
Also remove the spurious per-block print statement.
load_favorites() raised FileNotFoundError on first run before any favorites were ever saved. Guard with an existence check and return an empty list instead. save_favorites() assumed the parent directory already existed, which also fails on first run. Add os.makedirs(exist_ok=True) before opening the file for writing.
Two places logged sensitive information to stdout: 1. play_async() printed the full channel URL, which for Xtream providers contains the username and password embedded in the path (e.g. /live/user/pass/id.ts). 2. The reload() exception handler printed the raw provider_info string, which also contains the stored username and password. Remove the URL from the channel log line and replace the provider_info dump with a generic message.
Two bare 'except: pass' blocks swallow all exceptions including SystemExit, KeyboardInterrupt, and GeneratorExit, which can prevent the application from shutting down cleanly. Replace with 'except Exception: pass' to only catch regular exceptions.
os.chdir() changes the working directory for the entire process, so any code running concurrently or afterwards that relies on the CWD would break. Relative paths like './yt-dlp' also fail if the CWD changes again before the next call. Build an absolute path with os.path.join() and pass it directly to subprocess.getoutput() as a list (no shell involved). Replace the shell 'chmod a+rx' invocation with os.chmod() for the same reason.
GLib.timeout_add_seconds() requires an integer number of seconds. Passing 0.1 silently truncates to 0, which fires the callback immediately with no debounce delay. Replace with GLib.timeout_add(100, ...) which takes milliseconds, providing the intended 100 ms debounce.
on_search() accessed self.active_provider.channels without checking whether active_provider is None. If the user typed in the search bar before any provider was loaded or selected, this raised an AttributeError. Guard with an early return that re-enables the search bar so the user is not left with a frozen input.
set_markup("<b>%s</b>" % provider.name) passes the raw provider name
into a Pango markup string. A name containing '&', '<', or '>'
causes a Pango parse error and a GTK critical warning, breaking the
providers page layout.
Wrap the name with GLib.markup_escape_text() before interpolation.
The open_about() method opened the GPL file without a context manager, read it line-by-line into a list, then concatenated all lines in a loop. If an exception occurred mid-loop the file handle would leak. Replace with a 'with' block and a single h.read() call.
'not type(params) == dict' uses identity comparison on a type object. It does not recognise dict subclasses and reads less clearly than the idiomatic 'not isinstance(params, dict)'. Fix both occurrences in on_video_params() and on_audio_params().
'self.cache_path == ""' is a comparison that evaluates and discards True/False; it does not reassign the attribute. The intended assignment 'self.cache_path = ""' was never executed, so an invalid cache directory was never replaced with the default ~/.xtream-cache/.
'elif "Live":' evaluates the non-empty string literal as a boolean, which is always True. This means the else branch that prints an "Unrecognized stream type" warning can never be reached, and any stream type that is not "VOD" or "Series" silently becomes TV_GROUP instead of being reported as an error. Fix: 'elif stream_type == "Live":'
groups, channels, series, movies, auth_data, authorization, state, and catch_all_group were declared as class-level attributes with mutable default values (lists and dicts). In Python these are shared across all instances of the class, so a second XTream instance would inherit and mutate the first instance's data. Move all mutable attributes into __init__ so each instance gets its own independent copy. Also pass "Live" as the stream_type for catch_all_group instead of the empty string that previously produced an "Unrecognized stream type" warning on every construction.
After the if/else block that creates either new_series (series branch) or new_channel (live/vod branch), the code unconditionally evaluated new_channel.group_id. When loading series streams new_channel was never assigned, causing an UnboundLocalError crash on every series stream entry. Move the group_id == "9999" debug print inside the live and vod branches where new_channel is guaranteed to exist.
load_iptv() appended self.catch_all_group (the class-level instance) to self.groups at the start of each stream-type iteration, then appended a second freshly-constructed catch_all Group at the end of the same iteration. With three stream types (Live, VOD, Series) this produced up to six catch_all entries in self.groups. The per-stream-type Group added at the end of the loop is sufficient; remove the redundant prepend.
re.match(compiled_regex, string) re-enters the module to dispatch back to the compiled pattern object. Call regex.match(string) directly on the compiled object to skip the redundant dispatch. Also remove the dead 'if search_result is not None' guard: search_result is always a list and therefore never None, so the condition was always True and the inner block always executed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR addresses bugs, security issues, and minor quality problems found across
common.py,hypnotix.py, andxtream.py.common.py
os.system("mkdir -p '%s'" % PROVIDERS_PATH)replaced withos.makedirs(exist_ok=True)downloaded_byteswas incremented by the fixed 4 MB block constant instead of the actual number of bytes received, causing the size check to almost always fail and delete the freshly-downloaded playlistValueErrorload_favorites()raisedFileNotFoundErrorbefore any favorites existed;save_favorites()assumed the parent directory was already presenthypnotix.py
play_async()logged the full channel URL (which embeds Xtream username/password); thereload()exception handler logged the rawprovider_infostring (same credentials)exceptclauses – twoexcept: passblocks replaced withexcept Exception: passto avoid swallowingSystemExit/KeyboardInterruptos.chdir()–update_ytdlp()calledos.chdir()which changes the working directory for the entire process; replaced with absolute paths andos.chmod()GLib.timeout_add_seconds(0.1, ...)silently truncated0.1to0(integer required), firing immediately with no debounce; replaced withGLib.timeout_add(100, ...)on_search()accessedself.active_provider.channelswithout a None guardset_markup("<b>%s</b>" % provider.name)would raise a GTK critical error if the provider name contained&,<, or>; fixed withGLib.markup_escape_text()with/read()type() ==anti-pattern – twonot type(params) == dictchecks replaced withnot isinstance(params, dict)xtream.py
self.cache_path == ""(comparison) should beself.cache_path = ""; the fallback to~/.xtream-cache/never triggeredelif "Live":is alwaysTrue; fixed toelif stream_type == "Live":groups,channels,series,movies,auth_data,authorization,state, andcatch_all_groupwere class-level mutable objects shared across all instances; moved to__init__UnboundLocalErroron series streams –new_channel.group_idwas accessed unconditionally after an if/else where only the non-series branch createsnew_channel; guard moved inside the live/vod branchesself.catch_all_groupwas appended at the start of each stream-type iteration and a new catch_allGroupwas appended at the end, producing up to six catch_all entries; removed the redundant prependre.match(compiled_regex, name)replaced withregex.match(name); removed deadif search_result is not Noneguard (alwaysTrue)