-
Notifications
You must be signed in to change notification settings - Fork 845
Refactor api/view.catch_all to reduce cyclomatic complexity #123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
ma-maiorova
commented
Dec 5, 2025
- Extracted query parameter parsing into a dedicated ViewParams dataclass (api/view_params.py).
- Moved image- and color-related helpers into view_utils.py (e.g. loading cover image, base64 encoding, bar color extraction, artist/song resolution).
- Simplified catch_all into a linear sequence of small steps (validation, offline handling, redirect, cover loading, bar color selection, SVG rendering).
- Kept the public API and function names intact so existing tests and integrations continue to work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR successfully refactors the catch_all function in api/view.py to reduce cyclomatic complexity by extracting helper functions and introducing structured parameter handling. The refactoring maintains backward compatibility while improving code organization and readability.
Key Changes:
- Introduced
ViewParamsdataclass to encapsulate all query parameters in a structured format - Extracted image processing, color extraction, and artist/song resolution logic into dedicated utility functions in
view_utils.py - Simplified the main
catch_allflow into a clear, linear sequence of operations
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 10 comments.
| File | Description |
|---|---|
| api/view_params.py | New dataclass to hold parsed view query parameters, providing type safety and cleaner parameter passing |
| api/view_utils.py | New utility module containing extracted helper functions for image loading, base64 encoding, cover image handling, bar color extraction, and artist/song name resolution |
| api/view.py | Refactored main view handler with simplified logic flow, new parse_view_params() and build_offline_response() functions, and removal of inline logic in favor of utility function calls |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return "" | ||
| return b64encode(content).decode("ascii") | ||
|
|
||
| def load_cover_image_if_needed(cover_image, currently_playing_type, item, load_image_func, to_img_b64_func,): |
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Function signature has inconsistent trailing comma formatting. Line 34 has a trailing comma after to_img_b64_func, but line 49 has a trailing comma after colorgram_module,. This is inconsistent and can be confusing. Consider either removing all trailing commas from function parameter lists or applying them consistently only to the last parameter.
| song_name = item["name"] | ||
| bar_color = params.bar_color | ||
| if params.is_bar_color_from_cover and img is not None: | ||
| bar_color = extract_bar_color_from_image(img, params.theme, bar_color, isLightOrDark, colorgram) |
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function extract_bar_color_from_image is called with isLightOrDark and colorgram as parameters, but these are passed as module/function references rather than being called within the utility function itself. This creates an unusual dependency injection pattern that makes the code harder to understand and test. Consider importing and using isLightOrDark directly within extract_bar_color_from_image in view_utils.py, similar to how other dependencies are handled.
| bar_color = extract_bar_color_from_image(img, params.theme, bar_color, isLightOrDark, colorgram) | |
| bar_color = extract_bar_color_from_image(img, params.theme, bar_color) |
| @functools.lru_cache(maxsize=128) | ||
| def load_image(url): | ||
| try: | ||
| response = requests.get(url, timeout=10) | ||
| response.raise_for_status() | ||
| return response.content | ||
| except requests.exceptions.RequestException as e: | ||
| print(f"Error loading image from {url}: {e}") | ||
| # Return a placeholder or None to handle gracefully | ||
| return None | ||
| except Exception as e: | ||
| print(f"Unexpected error loading image: {e}") | ||
| return None | ||
|
|
||
| def to_img_b64(content): | ||
| if content is None: | ||
| return "" | ||
| return b64encode(content).decode("ascii") | ||
|
|
||
| def load_cover_image_if_needed(cover_image, currently_playing_type, item, load_image_func, to_img_b64_func,): | ||
| if not cover_image: | ||
| return None, "" | ||
|
|
||
| img = None | ||
| if currently_playing_type == "track": | ||
| img = load_image_func(item["album"]["images"][1]["url"]) | ||
| elif currently_playing_type == "episode": | ||
| img = load_image_func(item["images"][1]["url"]) | ||
|
|
||
| if img is None: | ||
| return None, "" | ||
|
|
||
| return img, to_img_b64_func(img) | ||
|
|
||
| def extract_bar_color_from_image(img, theme, default_bar_color, isLightOrDark_func, colorgram_module,): | ||
| is_skip_dark = theme in ["default"] | ||
|
|
||
| try: | ||
| pil_img = Image.open(io.BytesIO(img)) | ||
| colors = colorgram_module.extract(pil_img, 5) | ||
| except Exception as e: | ||
| print(f"Error extracting colors from image: {e}") | ||
| return default_bar_color | ||
|
|
||
| for color in colors: | ||
| rgb = color.rgb | ||
| light_or_dark = isLightOrDark_func([rgb.r, rgb.g, rgb.b], threshold=80) | ||
|
|
||
| if light_or_dark == "dark" and is_skip_dark: | ||
| # Skip to use bar in dark color | ||
| continue | ||
|
|
||
| return "%02x%02x%02x" % (rgb.r, rgb.g, rgb.b) | ||
|
|
||
| return default_bar_color | ||
|
|
||
| def resolve_artist_and_song_names(item, currently_playing_type): | ||
| if currently_playing_type == "track": | ||
| return item["artists"][0]["name"], item["name"] | ||
| if currently_playing_type == "episode": | ||
| return item["show"]["publisher"], item["name"] | ||
| return "", "" |
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The newly introduced utility functions in api/view_utils.py (load_image, to_img_b64, load_cover_image_if_needed, extract_bar_color_from_image, resolve_artist_and_song_names) and the dataclass ViewParams lack test coverage. Since the repository has comprehensive test coverage for the api/view.py module (as seen in tests/test_api_view.py), these newly extracted functions should also have dedicated unit tests to ensure their behavior is properly validated, especially edge cases like missing image indices or None returns.
| def load_cover_image_if_needed(cover_image, currently_playing_type, item, load_image_func, to_img_b64_func,): | ||
| if not cover_image: | ||
| return None, "" | ||
|
|
||
| img = None | ||
| if currently_playing_type == "track": | ||
| img = load_image_func(item["album"]["images"][1]["url"]) | ||
| elif currently_playing_type == "episode": | ||
| img = load_image_func(item["images"][1]["url"]) | ||
|
|
||
| if img is None: | ||
| return None, "" | ||
|
|
||
| return img, to_img_b64_func(img) |
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Parameter names load_image_func and to_img_b64_func use _func suffix which is redundant and verbose. Since these are dependency injection parameters, consider more concise names like image_loader and image_encoder, or simply remove the suffix: load_image and to_img_b64 (which wouldn't shadow the actual functions in this context).
| def load_cover_image_if_needed(cover_image, currently_playing_type, item, load_image_func, to_img_b64_func,): | |
| if not cover_image: | |
| return None, "" | |
| img = None | |
| if currently_playing_type == "track": | |
| img = load_image_func(item["album"]["images"][1]["url"]) | |
| elif currently_playing_type == "episode": | |
| img = load_image_func(item["images"][1]["url"]) | |
| if img is None: | |
| return None, "" | |
| return img, to_img_b64_func(img) | |
| def load_cover_image_if_needed(cover_image, currently_playing_type, item, load_image, to_img_b64,): | |
| if not cover_image: | |
| return None, "" | |
| img = None | |
| if currently_playing_type == "track": | |
| img = load_image(item["album"]["images"][1]["url"]) | |
| elif currently_playing_type == "episode": | |
| img = load_image(item["images"][1]["url"]) | |
| if img is None: | |
| return None, "" | |
| return img, to_img_b64(img) |
| def parse_view_params(): | ||
| uid = request.args.get("uid") | ||
| return ViewParams( | ||
| uid=uid, | ||
| cover_image=request.args.get("cover_image", default="true") == "true", | ||
| is_redirect=request.args.get("redirect", default="false") == "true", | ||
| theme=request.args.get("theme", default="default"), | ||
| bar_color=request.args.get("bar_color", default="53b14f"), | ||
| background_color=request.args.get("background_color", default="121212"), | ||
| is_bar_color_from_cover=( | ||
| request.args.get("bar_color_cover", default="false") == "true" | ||
| ), | ||
| show_offline=request.args.get("show_offline", default="false") == "true", | ||
| interchange=request.args.get("interchange", default="false") == "true", | ||
| mode=request.args.get("mode", default="light"), | ||
| is_enable_profanity=request.args.get("profanity", default="false") == "true", | ||
| ) |
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The parse_view_params function lacks test coverage. Since the repository has comprehensive test coverage for view.py (as seen in tests/test_api_view.py), this newly extracted parsing logic should have dedicated unit tests to validate parameter parsing, especially edge cases like missing parameters, invalid boolean strings, and default values.
| def build_offline_response(params, is_now_playing, progress_ms, duration_ms): | ||
| if params.interchange: | ||
| artist_name = "Currently not playing on Spotify" | ||
| song_name = "Offline" | ||
| else: | ||
| artist_name = "Offline" | ||
| song_name = "Currently not playing on Spotify" | ||
|
|
||
| svg = make_svg( | ||
| artist_name, | ||
| song_name, | ||
| img_b64="", | ||
| is_now_playing=is_now_playing, | ||
| cover_image=False, | ||
| theme=params.theme, | ||
| bar_color=params.bar_color, | ||
| show_offline=params.show_offline, | ||
| background_color=params.background_color, | ||
| mode=params.mode, | ||
| progress_ms=progress_ms, | ||
| duration_ms=duration_ms, | ||
| ) | ||
| resp = Response(svg, mimetype="image/svg+xml") | ||
| resp.headers["Cache-Control"] = "s-maxage=1" | ||
| return resp |
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The build_offline_response function lacks test coverage. While the offline behavior is tested in the existing tests (e.g., test_catch_all_with_valid_uid_offline), the new extracted function itself should have dedicated unit tests to ensure its logic is properly isolated and tested, including the interchange parameter handling.
| img = load_image_func(item["album"]["images"][1]["url"]) | ||
| elif currently_playing_type == "episode": | ||
| img = load_image_func(item["images"][1]["url"]) |
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The load_cover_image_if_needed function assumes that images have at least 2 items (index [1]) without validation. If the Spotify API returns an album or episode with fewer than 2 images, this will raise an IndexError. Consider adding a check: if len(item["album"]["images"]) >= 2: before accessing [1].
| img = load_image_func(item["album"]["images"][1]["url"]) | |
| elif currently_playing_type == "episode": | |
| img = load_image_func(item["images"][1]["url"]) | |
| images = item["album"]["images"] | |
| if len(images) >= 2: | |
| img = load_image_func(images[1]["url"]) | |
| elif len(images) >= 1: | |
| img = load_image_func(images[0]["url"]) | |
| elif currently_playing_type == "episode": | |
| images = item["images"] | |
| if len(images) >= 2: | |
| img = load_image_func(images[1]["url"]) | |
| elif len(images) >= 1: | |
| img = load_image_func(images[0]["url"]) |
| bar_color = "%02x%02x%02x" % (rgb.r, rgb.g, rgb.b) | ||
| break | ||
| img, img_b64 = load_cover_image_if_needed( | ||
| params.cover_image, currently_playing_type, item, load_image, to_img_b64, |
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trailing comma after to_img_b64, on line 367. While trailing commas in function calls can be acceptable, this appears inconsistent with the rest of the codebase. Consider removing it for consistency.
| params.cover_image, currently_playing_type, item, load_image, to_img_b64, | |
| params.cover_image, currently_playing_type, item, load_image, to_img_b64 |
| def extract_bar_color_from_image(img, theme, default_bar_color, isLightOrDark_func, colorgram_module,): | ||
| is_skip_dark = theme in ["default"] | ||
|
|
||
| try: | ||
| pil_img = Image.open(io.BytesIO(img)) | ||
| colors = colorgram_module.extract(pil_img, 5) | ||
| except Exception as e: | ||
| print(f"Error extracting colors from image: {e}") | ||
| return default_bar_color | ||
|
|
||
| for color in colors: | ||
| rgb = color.rgb | ||
| light_or_dark = isLightOrDark_func([rgb.r, rgb.g, rgb.b], threshold=80) |
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Parameter names isLightOrDark_func and colorgram_module use inconsistent naming conventions. isLightOrDark_func uses camelCase with _func suffix, while colorgram_module uses snake_case with _module suffix. For consistency and Python conventions (PEP 8), use snake_case: is_light_or_dark_func or better yet, light_dark_checker and color_extractor for more descriptive names.
| def extract_bar_color_from_image(img, theme, default_bar_color, isLightOrDark_func, colorgram_module,): | |
| is_skip_dark = theme in ["default"] | |
| try: | |
| pil_img = Image.open(io.BytesIO(img)) | |
| colors = colorgram_module.extract(pil_img, 5) | |
| except Exception as e: | |
| print(f"Error extracting colors from image: {e}") | |
| return default_bar_color | |
| for color in colors: | |
| rgb = color.rgb | |
| light_or_dark = isLightOrDark_func([rgb.r, rgb.g, rgb.b], threshold=80) | |
| def extract_bar_color_from_image(img, theme, default_bar_color, light_dark_checker, color_extractor,): | |
| is_skip_dark = theme in ["default"] | |
| try: | |
| pil_img = Image.open(io.BytesIO(img)) | |
| colors = color_extractor.extract(pil_img, 5) | |
| except Exception as e: | |
| print(f"Error extracting colors from image: {e}") | |
| return default_bar_color | |
| for color in colors: | |
| rgb = color.rgb | |
| light_or_dark = light_dark_checker([rgb.r, rgb.g, rgb.b], threshold=80) |
| img_b64="", | ||
| is_now_playing=is_now_playing, | ||
| cover_image=False, | ||
| theme=params.theme, | ||
| bar_color=params.bar_color, | ||
| show_offline=params.show_offline, | ||
| background_color=params.background_color, | ||
| mode=params.mode, | ||
| progress_ms=progress_ms, | ||
| duration_ms=duration_ms, |
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keyword argument 'img_b64' is not a supported parameter name of function make_svg.
| img_b64="", | |
| is_now_playing=is_now_playing, | |
| cover_image=False, | |
| theme=params.theme, | |
| bar_color=params.bar_color, | |
| show_offline=params.show_offline, | |
| background_color=params.background_color, | |
| mode=params.mode, | |
| progress_ms=progress_ms, | |
| duration_ms=duration_ms, | |
| "", | |
| is_now_playing, | |
| False, | |
| params.theme, | |
| params.bar_color, | |
| params.show_offline, | |
| params.background_color, | |
| params.mode, | |
| progress_ms, | |
| duration_ms, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.