Skip to content

Commit 648edc9

Browse files
Apply Technical Review suggestions
Co-authored-by: Bartosz <[email protected]>
1 parent 56c2360 commit 648edc9

File tree

9 files changed

+84
-102
lines changed

9 files changed

+84
-102
lines changed

python-selenium/README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,23 @@ Then, install the requirements:
1212
(venv) $ python -m pip install -r requirements.txt
1313
```
1414

15-
The only direct dependency for this project is [Selenium](https://selenium-python.readthedocs.io/). You should use a Python version of at least 3.8.
15+
The only direct dependency for this project is [Selenium](https://selenium-python.readthedocs.io/). You should use a Python version of at least 3.10, which is necessary to support [structural pattern matching](https://realpython.com/structural-pattern-matching/).
16+
17+
You'll need a [Firefox Selenium driver](https://selenium-python.readthedocs.io/installation.html#drivers) called `geckodriver` to run the project as-is. Make sure to [download and install](https://github.com/mozilla/geckodriver/releases) it before running the project.
1618

1719
## Run the Bandcamp Discover Player
1820

19-
To run the music placer, navigate to the `src/` folder, then execute the module from your command-line:
21+
To run the music player, install the package, then use the entry point command from your command-line:
2022

2123
```sh
22-
(venv) $ cd src/
23-
(venv) $ python -m bandcamp
24+
(venv) $ python -m pip install .
25+
(venv) $ bandcamp-player
2426
```
2527

2628
You'll see a text-based user interface that allows you to interact with the music player:
2729

2830
```
29-
Type: [play <track number>], [tracks], [more], [exit]
31+
Type: play [<track_number>] | tracks | more | exit
3032
>
3133
```
3234

@@ -39,4 +41,4 @@ Bartosz Zaczyński - Email: [email protected]
3941

4042
## License
4143

42-
Distributed under the MIT license. See ``LICENSE`` for more information.
44+
Distributed under the MIT license. See `LICENSE` for more information.

python-selenium/pyproject.toml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
55
[project]
66
name = "bandcamp_player"
77
version = "0.1.0"
8-
requires-python = ">=3.8"
8+
requires-python = ">=3.10"
99
description = "A web player for Bandcamp using Selenium"
1010
authors = [
1111
{ name = "Martin Breuss", email = "[email protected]" },
@@ -16,6 +16,3 @@ dependencies = [
1616
]
1717
[project.scripts]
1818
bandcamp-player = "bandcamp.__main__:main"
19-
20-
[tool.setuptools.packages.find]
21-
where = ["src"]

python-selenium/requirements.txt

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,15 @@
1-
appdirs==1.4.4
21
attrs==24.2.0
3-
certifi==2024.7.4
2+
certifi==2024.8.30
43
h11==0.14.0
5-
idna==3.7
6-
jedi==0.19.1
4+
idna==3.10
75
outcome==1.3.0.post0
8-
parso==0.8.4
9-
prompt_toolkit==3.0.47
10-
ptpython==3.0.29
11-
Pygments==2.18.0
126
PySocks==1.7.1
13-
selenium==4.23.1
7+
selenium==4.25.0
148
sniffio==1.3.1
159
sortedcontainers==2.4.0
16-
trio==0.26.1
10+
trio==0.27.0
1711
trio-websocket==0.11.1
1812
typing_extensions==4.12.2
19-
urllib3==2.2.2
20-
wcwidth==0.2.13
13+
urllib3==2.2.3
2114
websocket-client==1.8.0
2215
wsproto==1.2.0
Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
1-
from bandcamp.app.tui import TUI
1+
from bandcamp.app.tui import interact
22

33

44
def main():
5-
"""Provides the main entry point for the app."""
6-
tui = TUI()
7-
tui.interact()
8-
9-
10-
if __name__ == "__main__":
11-
main()
5+
"""Provide the main entry point for the app."""
6+
interact()

python-selenium/src/bandcamp/app/player.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
from bandcamp.web.element import TrackElement
55
from bandcamp.web.page import HomePage
66

7-
BANDCAMP_FRONTPAGE = "https://bandcamp.com/"
7+
BANDCAMP_FRONTPAGE_URL = "https://bandcamp.com/"
88

99

1010
class Player:
11-
"""Plays tracks from Bandcamp's Discover section."""
11+
"""Play tracks from Bandcamp's Discover section."""
1212

1313
def __init__(self) -> None:
1414
self._driver = self._set_up_driver()
@@ -22,11 +22,11 @@ def __enter__(self):
2222
return self
2323

2424
def __exit__(self, exc_type, exc_value, exc_tb):
25-
"""Closes the headless browser."""
25+
"""Close the headless browser."""
2626
self._driver.close()
2727

2828
def play(self, track_number=None):
29-
"""Plays the first track, or one of the available numbered tracks."""
29+
"""Play the first track, or one of the available numbered tracks."""
3030
if track_number:
3131
self._current_track = TrackElement(
3232
self.home.discover_tracklist.available_tracks[
@@ -37,9 +37,9 @@ def play(self, track_number=None):
3737
self._current_track.play()
3838

3939
def _set_up_driver(self):
40-
"""Creates a headless browser pointing to Bandcamp."""
40+
"""Create a headless browser pointing to Bandcamp."""
4141
options = Options()
4242
options.add_argument("--headless")
4343
browser = Firefox(options=options)
44-
browser.get(BANDCAMP_FRONTPAGE)
44+
browser.get(BANDCAMP_FRONTPAGE_URL)
4545
return browser
Lines changed: 46 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,61 @@
1-
from bandcamp.app.player import Player
2-
1+
"""Provide a text-based user interface for a Bandcamp music player."""
32

4-
class TUI:
5-
"""Provides a text-based user interface for a Bandcamp music player."""
3+
from bandcamp.app.player import Player
64

7-
COLUMN_WIDTH = CW = 30
5+
COLUMN_WIDTH = CW = 30
86

9-
def interact(self):
10-
"""Controls the player through user interactions."""
11-
with Player() as player:
12-
while True:
13-
print(
14-
"\nType: [play <track number>], [tracks], [more], [exit]"
15-
)
16-
command = input("> ").strip().lower()
177

18-
if command.startswith("play"):
8+
def interact():
9+
"""Control the player through user interactions."""
10+
with Player() as player:
11+
while True:
12+
print("\nType: play [<track_number>] | tracks | more | exit")
13+
match input("> ").strip().lower().split():
14+
case ["play"]:
15+
play(player)
16+
case ["play", track]:
1917
try:
20-
track_number = int(command.split()[1])
21-
self.play(player, track_number)
22-
except IndexError: # Play first track.
23-
self.play(player)
18+
track_number = int(track)
19+
play(player, track_number)
2420
except ValueError:
2521
print("Please provide a valid track number.")
26-
elif command == "tracks":
27-
self.tracks(player)
28-
elif command == "more":
22+
case ["tracks"]:
23+
display_tracks(player)
24+
case ["more"]:
2925
player.discover.load_more()
30-
self.tracks(player)
31-
elif command == "exit":
26+
display_tracks(player)
27+
case ["exit"]:
3228
print("Exiting the player...")
3329
break
34-
else:
30+
case _:
3531
print("Unknown command. Try again.")
3632

37-
def play(self, player, track_number=None):
38-
"""Plays a track and shows info about the track."""
39-
player.play(track_number)
40-
print(player._current_track._get_track_info())
4133

42-
def tracks(self, player):
43-
"""Displays information about the currently playable tracks."""
44-
header = (
45-
f"{'#':<5} {'Album':<{self.CW}} "
46-
f"{'Artist':<{self.CW}} "
47-
f"{'Genre':<{self.CW}}"
34+
def play(player, track_number=None):
35+
"""Play a track and show info about the track."""
36+
player.play(track_number)
37+
print(player._current_track._get_track_info())
38+
39+
40+
def display_tracks(player):
41+
"""Display information about the currently playable tracks."""
42+
header = (
43+
f"{'#':<5} {'Album':<{CW}} " f"{'Artist':<{CW}} " f"{'Genre':<{CW}}"
44+
)
45+
print(header)
46+
print("-" * 100)
47+
for track_number, track in enumerate(
48+
player.discover.available_tracks, start=1
49+
):
50+
album, artist, *genre = track.text.split("\n")
51+
album = _truncate(album, CW)
52+
artist = _truncate(artist, CW)
53+
genre = _truncate(genre[0], CW) if genre else ""
54+
print(
55+
f"{track_number:<5} {album:<{CW}} " f"{artist:<{CW}} {genre:<{CW}}"
4856
)
49-
print(header)
50-
print("-" * 100)
51-
for track_number, track in enumerate(
52-
player.discover.available_tracks, start=1
53-
):
54-
album, artist, *genre = track.text.split("\n")
55-
album = self._truncate(album, self.CW)
56-
artist = self._truncate(artist, self.CW)
57-
genre = self._truncate(genre[0], self.CW) if genre else ""
58-
print(
59-
f"{track_number:<5} {album:<{self.CW}} "
60-
f"{artist:<{self.CW}} {genre:<{self.CW}}"
61-
)
6257

63-
@staticmethod
64-
def _truncate(text, width):
65-
"""Truncates track information."""
66-
return text[: width - 3] + "..." if len(text) > width else text
58+
59+
def _truncate(text, width):
60+
"""Truncate track information."""
61+
return text[: width - 3] + "..." if len(text) > width else text

python-selenium/src/bandcamp/web/element.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
from selenium.webdriver.remote.webelement import WebElement
33

44
from bandcamp.web.base import Track, WebComponent
5-
from bandcamp.web.locators import HomePageLocatorMixin, TrackLocatorMixin
5+
from bandcamp.web.locators import HomePageLocator, TrackLocator
66

77

8-
class TrackElement(WebComponent, TrackLocatorMixin):
9-
"""Models a playable track in Bandcamp's Discover section."""
8+
class TrackElement(WebComponent, TrackLocator):
9+
"""Model a playable track in Bandcamp's Discover section."""
1010

1111
def play(self) -> None:
12-
"""Plays the track."""
12+
"""Play the track."""
1313
if not self.is_playing():
1414
self._get_play_button().click()
1515
self._wait.until(lambda _: self.is_playing())
@@ -18,7 +18,7 @@ def is_playing(self) -> bool:
1818
return "playing" in self._get_play_button().get_attribute("class")
1919

2020
def _get_track_info(self) -> Track:
21-
"""Creates a representation of the track's relevant information."""
21+
"""Create a representation of the track's relevant information."""
2222
full_url = self._parent.find_element(*self.ALBUM).get_attribute("href")
2323
# Cut off the referrer query parameter
2424
clean_url = full_url.split("?")[0] if full_url else ""
@@ -33,23 +33,23 @@ def _get_play_button(self):
3333
return self._parent.find_element(*self.PLAY_BUTTON)
3434

3535

36-
class DiscoverTrackList(WebComponent, HomePageLocatorMixin):
37-
"""Models the track list in Bandcamp's Discover section."""
36+
class DiscoverTrackList(WebComponent, HomePageLocator):
37+
"""Model the track list in Bandcamp's Discover section."""
3838

3939
def __init__(self, parent: WebElement, driver: WebDriver = None) -> None:
4040
super().__init__(parent, driver)
4141
self.available_tracks = self._get_available_tracks()
4242

4343
def load_more(self) -> None:
44-
"""Loads additional tracks in the Discover section."""
44+
"""Load additional tracks in the Discover section."""
4545
self._get_next_page_button().click()
4646
self.available_tracks = self._get_available_tracks()
4747

4848
def _get_available_tracks(self) -> list:
49-
"""Finds all currently available tracks in the Discover section."""
49+
"""Find all currently available tracks in the Discover section."""
5050
all_tracks = self._driver.find_elements(*self.TRACK)
5151
return [track for track in all_tracks if track.is_displayed()]
5252

5353
def _get_next_page_button(self):
54-
"""Locates and returns the 'Next' button that loads more results."""
54+
"""Locate and return the 'Next' button that loads more results."""
5555
return self._driver.find_elements(*self.PAGINATION_BUTTON)[-1]

python-selenium/src/bandcamp/web/locators.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
from selenium.webdriver.common.by import By
22

33

4-
class HomePageLocatorMixin:
4+
class HomePageLocator:
55
DISCOVER_RESULTS = (By.CLASS_NAME, "discover-results")
66
TRACK = (By.CLASS_NAME, "discover-item")
77
PAGINATION_BUTTON = (By.CLASS_NAME, "item-page")
88

99

10-
class TrackLocatorMixin:
10+
class TrackLocator:
1111
PLAY_BUTTON = (By.CSS_SELECTOR, "a")
1212
ALBUM = (By.CLASS_NAME, "item-title")
1313
GENRE = (By.CLASS_NAME, "item-genre")

python-selenium/src/bandcamp/web/page.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
from bandcamp.web.base import WebPage
44
from bandcamp.web.element import DiscoverTrackList
5-
from bandcamp.web.locators import HomePageLocatorMixin
5+
from bandcamp.web.locators import HomePageLocator
66

77

8-
class HomePage(WebPage, HomePageLocatorMixin):
9-
"""Models the relevant parts of the Bandcamp home page."""
8+
class HomePage(WebPage, HomePageLocator):
9+
"""Model the relevant parts of the Bandcamp home page."""
1010

1111
def __init__(self, driver: WebDriver) -> None:
1212
super().__init__(driver)

0 commit comments

Comments
 (0)